约定式路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
my-nuxt-app/
├─ app/
│ ├─ assets/ // 静态资源
│ ├─ components/ // 组件
│ ├─ composables/ // 组合式 API 函数
│ ├─ layouts/ // 布局(default.vue)
│ ├─ middleware/ // 中间件
│ ├─ pages/ // 页面(index.vue为默认页,[param].vue)
│ ├─ plugins/ // 注入全局功能
│ ├─ utils/ // 工具函数
│ ├─ app.vue
│ ├─ app.config.ts
│ └─ error.vue // 错误页面
├─ content/ // 需要安装@nuxt/content,将md转换为page
├─ public/ // 直接暴露给浏览器的静态文件
├─ shared/
├─ server/ // 后端逻辑(./api/)
└─ nuxt.config.ts
|
Hooks概念
useFetch, &Fetch, useLazyFetch,useAsyncData
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
useFetch
在 服务端渲染时执行,首屏数据已经带在 HTML 中(SEO 友好);
在 客户端导航时 会自动重新请求;
会自动缓存、解包响应(JSON 自动解析);
<script setup lang="ts">
const { data, pending, error } = await useFetch('/api/user')
</script>
<template>
<div v-if="pending">加载中...</div>
<div v-else>{{ data }}</div>
</template>
const updatedUser = { id: 1, name: 'Updated Name' }
const { data, error } = await useFetch(`/api/users/${updatedUser.id}`, {
method: 'PUT', // 或 'PATCH'
body: updatedUser,
params: {
page: page.value,
limit: limit.value
}
})
$fetch
没有 SSR 自动序列化;
返回原始 Promise;
适合在 composable、middleware、server/api 或 事件回调中 使用;
可在服务端和客户端运行;
性能最高、控制最灵活。
useLazyFetch
类似 useFetch(),但不会在页面渲染时阻塞 SSR;
页面先渲染框架,然后异步加载数据;
适合非关键性数据(比如用户统计、推荐、评论等)。
const { data, pending } = useLazyFetch('/api/stats')
<template>
<div>页面主内容</div>
<div v-if="pending">加载统计中...</div>
<div v-else>{{ data }}</div>
</template>
useAsyncData
执行任意异步函数
<script setup lang="ts">
const page = ref(1)
const { data: posts } = await useAsyncData(
'posts',
() => $fetch('https://fakeApi.com/posts', {
params: {
page: page.value
}
}), {
watch: [page]
}
)
</script>
|
useAsyncData
useCookeis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
Create:
// 创建一个 cookie(自动创建)
const token = useCookie('token', {
maxAge: 3600, // 1小时
path: '/',
secure: true, // 只在 HTTPS 下有效
sameSite: 'lax'
})
// 设置值
token.value = 'abc123'
Read:
const token = useCookie('token')
console.log(token.value) // "abc123"
Update:
const token = useCookie('token')
token.value = 'xyz789' // 会覆盖原值
Delete:
const token = useCookie('token')
token.value = null
|
useState
1
|
const count = useState('counter', () => Math.round(Math.random() * 100))
|
标签概念
NuxtLink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
//会翻译为<a>
<NuxtLink to="/about">关于页面</NuxtLink>
<NuxtLink :to="{ path: '/user', query: { id: 123, name: 'jhan' } }">打开用户页</NuxtLink>
// 接收
<script setup lang="ts">
const route = useRoute()
console.log(route.query.id) // "123"
console.log(route.query.name) // "jhan"
</script>
// 链接到静态文件
<NuxtLink to="/example-report.pdf" external>
下载报告
</NuxtLink>
<NuxtLink to="https://nuxtjs.org">
Nuxt 官网
</NuxtLink>
//activeClass 当前页面
<NuxtLink to="/" class="section-base block mb-2 p-3 rounded-lg no-underline text-gray-800 " activeClass="text-red-400">
<h2 class="font-semibold">仪表盘</h2>
<p class="text-sm text-gray-600">查看统计数据</p>
</NuxtLink>
|
NuxtImg
1
|
npx nuxt module add image
|
https://image.nuxt.com/usage/nuxt-img
1
|
<NuxtImg src="/nuxt-icon.png" />
|
其他概念
自定义layout
SSR/CSR 切换
SEO 优化与 sitemap
dotenv
首先需要注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// 服务端可用
apiSecret: process.env.API_SECRET,
databaseUrl: process.env.DATABASE_URL,
// 客户端也能访问
public: {
apiBase: process.env.PUBLIC_API_BASE
}
}
});
Usage:
back-end
export default defineEventHandler((event) => {
const config = useRuntimeConfig();
return {
secret: config.apiSecret, // 服务端变量
db: config.databaseUrl, // 服务端变量
base: config.public.apiBase // 客户端/服务端都能访问
};
});
front-end
<script setup lang="ts">
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
console.log('前端 API 地址:', apiBase)
</script>
|
构建 server api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
例子:
server/
└── api/
├── devices.ts // GET /api/devices
├── devices/[id].ts // GET /api/devices/101
├── devices/[id].put.ts // PUT /api/devices/101
├── devices/[id].delete.ts // DELETE /api/devices/101
└── devices.post.ts // POST /api/devices
~/server/api/foo/[...].ts // 捕获所有路由
// server/api/devices/[id].ts
export default defineEventHandler(async (event) => {
// 1. 获取 params (如 /devices/101)
const id = Number(event.context.params?.id)
// 2. 获取cookies
const cookies = parseCookies(event);
// 3. 获取 body (POST 数据)
const body = await readBody(event)
// 4. 获取 query 参数(如 ?page=1)
const query = getQuery(event)
const page = Number(query.page || 1)
// 5. 使用中间件注入的 user
const user = event.context.user
// 返回数据
return {
id,
user,
cookies,
body,
page
}
})
~/server/middleware/
auth.js
export default defineEventHandler((event) => {
event.context.auth = { user: 123 }
})
|
错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
export default defineEventHandler((event) => {
const id = parseInt(event.context.params.id) as number
if (!Number.isInteger(id)) {
throw createError({
statusCode: 400,
statusMessage: 'ID 应为整数',
})
}
return '一切正常'
})
状态码
export default defineEventHandler((event) => {
setResponseStatus(event, 202)
})
请求 Cookie
export default defineEventHandler((event) => {
const cookies = parseCookies(event)
return { cookies }
})
|
神人TV之如何解决特定路径中间件问题
1
2
3
4
5
|
export default defineEventHandler(async (event) => {
if (!event.path.startsWith("/api/hello")) return;
console.log("Hello middleware called");
});
|
使用插件
ElementPlus
暂未解决day.js问题
pinia
1
|
yarn add pinia @pinia/nuxt
|
持久化
SSR 仅支持 cookies
docs
npm i pinia-plugin-persistedstate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'My Counter'
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
reset() {
this.count = 0
}
},
persist: {
storage: piniaPluginPersistedstate.cookies(),
}
}
)
|
animate.css
“anime.css manual”
1
2
3
4
5
6
|
yarn add animate.css
export default defineNuxtConfig({
css: [
'animate.css/animate.min.css',
],
|
tailwindcss, unocss
模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
error.vue
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError
})
const route = useRoute();
const isHome = route.path === '/';
const open2 = () => {
ElNotification({
title: 'Custom Position',
message: "I'm at the bottom right corner",
position: 'bottom-right',
type:"warning"
})
}
</script>
<template>
<div class="flex min-h-screen items-center justify-center bg-gray-900 p-4">
<div class="text-center text-white max-w-md w-full space-y-6 animate__animated animate__fadeIn">
<Icon name="material-symbols:error-outline-rounded" size="64" class="text-white" />
<h1 class="text-5xl font-bold">{{ error.statusCode }}</h1>
<p class="text-gray-300 text-lg">
{{ error.statusMessage || '页面加载失败' }}
</p>
<button
v-if="isHome"
@click="open2()"
class="inline-block px-5 py-2.5 rounded-lg bg-green-600 text-white hover:bg-green-500 transition"
>
联系咨询
</button>
<NuxtLink
v-else
to="/"
class="inline-block px-5 py-2.5 rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition"
>
返回首页
</NuxtLink>
</div>
</div>
</template>
image.vue
<template>
<div
class="relative w-2xs h-2xs overflow-hidden rounded-lg hover:brightness-[0.7] transform duration-500 shadow-2xl hover:shadow-none flex items-center justify-center"
>
<div v-if="hasError" class="absolute w-full h-full flex flex-col items-center justify-center text-4xl text-red-900">
<Icon name="material-symbols:error-outline" />
<p class="text-lg mt-2">Loading error!</p>
</div>
<a
v-show="!hasError"
:href="src"
data-fancybox="gallery"
:data-caption="alt"
>
<NuxtImg
:src="src"
:alt="alt || ''"
format="webp"
densities="x1 x2"
class="object-cover w-full h-full transition-opacity duration-300 ease-in-out cursor-pointer"
@load="onImageLoad"
@error="onImageError"
/>
</a>
<div
v-if="isLoading && !hasError"
class="absolute inset-0 bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 bg-[length:200%_100%] animate-shimmer z-0"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Fancybox } from '@fancyapps/ui'
import '@fancyapps/ui/dist/fancybox/fancybox.css'
const props = defineProps({
src: {
type: String,
required: true
},
alt: {
type: String,
default: ''
}
})
const isLoading = ref(true)
const hasError = ref(false)
const onImageLoad = () => {
isLoading.value = false
hasError.value = false
}
const onImageError = () => {
isLoading.value = false
hasError.value = true
console.warn(`Image failed to load: ${props.src}`)
}
onMounted(() => {
Fancybox.bind('[data-fancybox="gallery"]', {})
})
</script>
<style scoped>
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.animate-shimmer {
animation: shimmer 1.5s infinite linear;
}
</style>
|
icon
“iconfiy”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
npx nuxi module add icon
uasge:
<Icon name="material-symbols:14mp" style="color: #000fff" :size="38"/>
如何查看icon名称?
Component - Iconfiy Icon
减少服务器存储的bundle
export default defineNuxtConfig({
modules: ['@nuxt/icon'],
icon: {
serverBundle: {
collections: ['uil', 'mdi']
}
}
})
|
杂项
懒加载