返回

Nuxt4笔记

目录

约定式路由

 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
适合在 composablemiddlewareserver/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))

标签概念

 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']
    }
  }
})

杂项

懒加载

Licensed under CC BY-NC-SA 4.0