mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 09:25:54 +00:00
feat(pages): page layouts (#276)
This commit is contained in:
parent
8faf069778
commit
b1948b1921
@ -2,8 +2,7 @@
|
||||
|
||||
The new `app.vue` file, is the main component in your Nuxt3 applications.
|
||||
|
||||
If you would like to customize default layout of website, create `app.vue` in your project directory. Nuxt will automatically detect it and load it as the parent of all other pages within your application.
|
||||
|
||||
If you would like to customize the global website entrypoint, you can create `app.vue` in your project directory. Nuxt will automatically detect it and load it as the parent of all other pages within your application. This will be primarily useful for adding code that should be run on every single page of your site (see [page layouts](/app/pages#layouts) creating dynamic layouts).
|
||||
|
||||
**Note:** Don't forget to use `<NuxtPage>` component somewhere inside `app.vue`.
|
||||
|
||||
|
@ -2,9 +2,11 @@
|
||||
|
||||
Nuxt will automatically integrate [Vue Router](https://next.router.vuejs.org/) and map `pages/` directory into the routes of your application.
|
||||
|
||||
## Dynamic Routes
|
||||
|
||||
If you place anything within square brackets, it will be turned into a [dynamic route](https://next.router.vuejs.org/guide/essentials/dynamic-matching.html) parameter. You can mix and match multiple parameters and even non-dynamic text within a file name or directory.
|
||||
|
||||
## Example
|
||||
### Example
|
||||
|
||||
```bash
|
||||
-| pages/
|
||||
@ -21,3 +23,59 @@ Given the example above, you can access group/userid within your component via t
|
||||
{{ $route.params.id }}
|
||||
</template>
|
||||
```
|
||||
|
||||
## Layouts
|
||||
|
||||
Nuxt provides a customizable layouts framework you can use throughout your application, ideal for extracting common UI or code patterns into reusable layout components.
|
||||
|
||||
Page layouts are placed in the `layouts/` directory and will be automatically loaded via asynchronous import when used. If you create a `layouts/default.vue` this will be used for all pages in your app. Other layouts are used by setting a `layout` property as part of your component options.
|
||||
|
||||
If you have only single layout for application, you can alternatively use (app entry)[/app].
|
||||
|
||||
### Example: a custom layout
|
||||
|
||||
```bash
|
||||
-| layouts/
|
||||
---| custom.vue
|
||||
```
|
||||
|
||||
In your layout files, you'll need to use `<slot />` to define where the page content of your layout will be loaded. For example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
Some shared layout content:
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Given the example above, you can use a custom layout like this:
|
||||
|
||||
```vue
|
||||
<script>
|
||||
export default {
|
||||
layout: "custom",
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Example: using with slots
|
||||
|
||||
You can also take full control (for example, with slots) by using the `<NuxtLayout>` component (which is globally available throughout your application) and set `layout: false` in your component options.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<NuxtLayout name="custom">
|
||||
<template #header> Some header template content. </template>
|
||||
|
||||
The rest of the page
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: false,
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
18
examples/with-layouts/layouts/custom.vue
Normal file
18
examples/with-layouts/layouts/custom.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<strong>Custom layout</strong>
|
||||
Header slot:
|
||||
<slot name="header">
|
||||
Default slot content
|
||||
</slot>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
created () {
|
||||
console.log('layout created')
|
||||
}
|
||||
}
|
||||
</script>
|
6
examples/with-layouts/layouts/default.vue
Normal file
6
examples/with-layouts/layouts/default.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
Default layout
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
6
examples/with-layouts/layouts/other.vue
Normal file
6
examples/with-layouts/layouts/other.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
Other
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
12
examples/with-layouts/package.json
Normal file
12
examples/with-layouts/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "example-with-layouts",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"nuxt3": "latest"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nu dev",
|
||||
"build": "nu build",
|
||||
"start": "node .output/server"
|
||||
}
|
||||
}
|
20
examples/with-layouts/pages/index.vue
Normal file
20
examples/with-layouts/pages/index.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
Welcome to Nuxt Layouts 👋
|
||||
|
||||
<NuxtLink to="/manual">
|
||||
Manual layout
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/same">
|
||||
Same layout
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineNuxtComponent } from '@nuxt/app'
|
||||
|
||||
export default defineNuxtComponent({
|
||||
layout: 'custom'
|
||||
})
|
||||
</script>
|
27
examples/with-layouts/pages/manual.vue
Normal file
27
examples/with-layouts/pages/manual.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
Manual control
|
||||
<NuxtLayout :name="layout">
|
||||
Default slot
|
||||
<button @click="layout ? layout = null : layout = 'custom'">
|
||||
Switch layout
|
||||
</button>
|
||||
|
||||
<template #header>
|
||||
Header slot
|
||||
</template>
|
||||
</NuxtLayout>
|
||||
<NuxtLink to="/">
|
||||
Back to home
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: false,
|
||||
data: () => ({
|
||||
layout: 'custom'
|
||||
})
|
||||
}
|
||||
</script>
|
14
examples/with-layouts/pages/same.vue
Normal file
14
examples/with-layouts/pages/same.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
Same layout as home
|
||||
<NuxtLink to="/">
|
||||
Back to home
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'custom'
|
||||
}
|
||||
</script>
|
@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^0.6.4",
|
||||
"globby": "^11.0.4",
|
||||
"scule": "^0.2.1",
|
||||
"ufo": "^0.7.5",
|
||||
"upath": "^2.0.1",
|
||||
"vue-router": "^4.0.10"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { existsSync } from 'fs'
|
||||
import { defineNuxtModule } from '@nuxt/kit'
|
||||
import { resolve } from 'upath'
|
||||
import { resolvePagesRoutes } from './utils'
|
||||
import { resolveLayouts, resolvePagesRoutes } from './utils'
|
||||
|
||||
export default defineNuxtModule({
|
||||
name: 'router',
|
||||
@ -12,7 +12,8 @@ export default defineNuxtModule({
|
||||
|
||||
nuxt.hook('builder:watch', async (event, path) => {
|
||||
// Regenerate templates when adding or removing pages (plugin and routes)
|
||||
if (event !== 'change' && path.startsWith('pages/')) {
|
||||
const pathPattern = new RegExp(`^(${nuxt.options.dir.pages}|${nuxt.options.dir.layouts})/`)
|
||||
if (event !== 'change' && path.match(pathPattern)) {
|
||||
await nuxt.callHook('builder:generateApp')
|
||||
}
|
||||
})
|
||||
@ -54,6 +55,22 @@ export default defineNuxtModule({
|
||||
return `export default ${JSON.stringify(serializedRoutes, null, 2).replace(/"{(.+)}"/g, '$1')}`
|
||||
}
|
||||
})
|
||||
|
||||
const layouts = await resolveLayouts(nuxt)
|
||||
|
||||
// Add routes.js
|
||||
app.templates.push({
|
||||
path: 'layouts.js',
|
||||
compile: () => {
|
||||
const layoutsObject = Object.fromEntries(layouts.map(({ name, file }) => {
|
||||
return [name, `{defineAsyncComponent({ suspensible: false, loader: () => import('${file}') })}`]
|
||||
}))
|
||||
return [
|
||||
'import { defineAsyncComponent } from \'vue\'',
|
||||
`export default ${JSON.stringify(layoutsObject, null, 2).replace(/"{(.+)}"/g, '$1')}
|
||||
`].join('\n')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
24
packages/pages/src/runtime/layout.ts
Normal file
24
packages/pages/src/runtime/layout.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
import layouts from '#build/layouts'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: [String, Boolean],
|
||||
default: 'default'
|
||||
}
|
||||
},
|
||||
setup (props, context) {
|
||||
return () => {
|
||||
const layout = props.name
|
||||
if (!layouts[layout]) {
|
||||
if (process.dev && layout && layout !== 'default') {
|
||||
console.warn(`Invalid layout \`${layout}\` selected.`)
|
||||
}
|
||||
return context.slots.default()
|
||||
}
|
||||
return h(layouts[layout], props, context.slots)
|
||||
}
|
||||
}
|
||||
})
|
@ -1,17 +1,53 @@
|
||||
<template>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<NuxtLayout :name="layout || updatedComponentLayout || Component.type.layout">
|
||||
<transition name="page" mode="out-in">
|
||||
<!-- <keep-alive> -->
|
||||
<Suspense @pending="$nuxt.callHook('page:start')" @resolve="$nuxt.callHook('page:finish')">
|
||||
<Suspense @pending="() => onSuspensePending(Component)" @resolve="() => onSuspenseResolved(Component)">
|
||||
<component :is="Component" :key="$route.path" />
|
||||
</Suspense>
|
||||
<!-- <keep-alive -->
|
||||
</transition>
|
||||
</NuxtLayout>
|
||||
</RouterView>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCurrentInstance, ref } from 'vue'
|
||||
|
||||
import NuxtLayout from './layout'
|
||||
|
||||
export default {
|
||||
name: 'NuxtPage'
|
||||
name: 'NuxtPage',
|
||||
components: { NuxtLayout },
|
||||
props: {
|
||||
layout: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
// Disable HMR reactivity in production
|
||||
const updatedComponentLayout = process.dev ? ref(null) : null
|
||||
|
||||
const { $nuxt } = getCurrentInstance().proxy
|
||||
|
||||
function onSuspensePending (Component) {
|
||||
if (process.dev) {
|
||||
updatedComponentLayout.value = Component.type.layout || null
|
||||
}
|
||||
return $nuxt.callHook('page:start', Component)
|
||||
}
|
||||
|
||||
function onSuspenseResolved (Component) {
|
||||
return $nuxt.callHook('page:finish', Component)
|
||||
}
|
||||
|
||||
return {
|
||||
updatedComponentLayout,
|
||||
onSuspensePending,
|
||||
onSuspenseResolved
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
// @ts-ignore
|
||||
import { defineNuxtPlugin } from '@nuxt/app'
|
||||
import NuxtPage from './page.vue'
|
||||
import NuxtLayout from './layout'
|
||||
// @ts-ignore
|
||||
import routes from '#build/routes'
|
||||
|
||||
@ -15,6 +16,7 @@ export default defineNuxtPlugin((nuxt) => {
|
||||
const { app } = nuxt
|
||||
|
||||
app.component('NuxtPage', NuxtPage)
|
||||
app.component('NuxtLayout', NuxtLayout)
|
||||
app.component('NuxtLink', RouterLink)
|
||||
|
||||
const routerHistory = process.client
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { extname, relative, resolve } from 'upath'
|
||||
import { basename, extname, relative, resolve } from 'upath'
|
||||
import { encodePath } from 'ufo'
|
||||
import { Nuxt, resolveFiles } from '@nuxt/kit'
|
||||
import { kebabCase } from 'scule'
|
||||
|
||||
export interface NuxtRoute {
|
||||
name?: string
|
||||
@ -215,3 +216,13 @@ function prepareRoutes (routes: NuxtRoute[], parent?: NuxtRoute) {
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
export async function resolveLayouts (nuxt: Nuxt) {
|
||||
const layoutDir = resolve(nuxt.options.srcDir, nuxt.options.dir.layouts)
|
||||
const files = await resolveFiles(layoutDir, `*{${nuxt.options.extensions.join(',')}}`)
|
||||
|
||||
return files.map((file) => {
|
||||
const name = kebabCase(basename(file).replace(extname(file), '')).replace(/["']/g, '')
|
||||
return { name, file }
|
||||
})
|
||||
}
|
||||
|
@ -1782,6 +1782,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@nuxt/kit": ^0.6.4
|
||||
globby: ^11.0.4
|
||||
scule: ^0.2.1
|
||||
ufo: ^0.7.5
|
||||
unbuild: ^0.3.1
|
||||
upath: ^2.0.1
|
||||
@ -6007,6 +6008,14 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"example-with-layouts@workspace:examples/with-layouts":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "example-with-layouts@workspace:examples/with-layouts"
|
||||
dependencies:
|
||||
nuxt3: latest
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"example-with-vite@workspace:examples/with-vite":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "example-with-vite@workspace:examples/with-vite"
|
||||
|
Loading…
Reference in New Issue
Block a user