Nuxt本地数据渲染
2025-08-30
集成
集成Arco
- 路径下回自动作为插件
/app/plugins
1import { defineNuxtPlugin } from '#app'
2import ArcoVue from '@arco-design/web-vue'
3import '@arco-design/web-vue/dist/arco.css'
4import {Icon} from '@arco-design/web-vue';
5import ArcoVueIcon from '@arco-design/web-vue/es/icon';
6
7const href = '//at.alicdn.com/t/c/font_4965719_1n8loeijakd.js';
8
9/**
10 * 阿里图标
11 */
12export const IconFont = Icon.addFromIconFontCn({
13 src: `https://${href}`,
14 extraProps: {
15 // fill: 'white',
16 },
17});
18
19
20export default defineNuxtPlugin((nuxtApp) => {
21 nuxtApp.vueApp.use(ArcoVue)
22 nuxtApp.vueApp.component('Iconfont', IconFont);
23 nuxtApp.vueApp.use(ArcoVueIcon);
24})
25引入tailwind
text
1npm install tailwindcss @tailwindcss/vite
2
3import tailwindcss from "@tailwindcss/vite";
4export default defineNuxtConfig({
5 compatibilityDate: "2025-07-15",
6 devtools: { enabled: true },
7 css: ['~/assets/css/tailwind.css'],
8 vite: {
9 plugins: [
10 tailwindcss(),
11 ],
12 },
13});
14app/assets/css/tailwind.css
1@import "tailwindcss";统一ts类型
shared/types/index.d.ts
其他地方可以直接引用
1import type {UserDO} from "#shared/types";Server
api接口统一包装
- server/utils/handler.ts
server/utils/handler.ts
1import type {EventHandler, EventHandlerRequest} from 'h3'
2
3export const defineWrappedResponseHandler = <T extends EventHandlerRequest, D>(
4 handler: EventHandler<T, D>
5): EventHandler<T, D> =>
6 defineEventHandler<T>(async event => {
7 try {
8 // 在路由处理器之前执行某些操作
9 const response = await handler(event)
10 // 在路由处理器之后执行某些操作
11 return {code: 0, data: response, msg: 'success'}
12 } catch (err) {
13 // 错误处理
14 return {code: -1, data: null, msg: err + ''}
15 }
16 })
17统一请求日志
- server/middleware/log.ts
server/middleware/log.ts
1import {defineEventHandler, getRequestURL, getRequestHeader, readBody} from 'h3'
2
3// ANSI 颜色码
4const reset = "x1b[0m"
5const red = "x1b[31m"
6const green = "x1b[32m"
7const yellow = "x1b[33m"
8const blue = "x1b[34m"
9const magenta = "x1b[35m"
10const cyan = "x1b[36m"
11const gray = "x1b[90m"
12
13function formatDate(date: Date) {
14 const pad = (n: number, width = 2) => n.toString().padStart(width, '0')
15 const year = date.getFullYear()
16 const month = pad(date.getMonth() + 1)
17 const day = pad(date.getDate())
18 const hour = pad(date.getHours())
19 const minute = pad(date.getMinutes())
20 const second = pad(date.getSeconds())
21 const ms = date.getMilliseconds().toString().padStart(3, '0')
22 return `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms}`
23}
24
25function getClientIP(event: any) {
26 const forwarded = event.node.req.headers['x-forwarded-for'] as string
27 if (forwarded) return forwarded.split(',')[0].trim() // 取第一个真实 IP
28 const ip = event.node.req.socket?.remoteAddress || ''
29 return ip.replace('::ffff:', '') // IPv6 映射 IPv4 时去掉前缀
30}
31
32export default defineEventHandler(async (event) => {
33 const url = event.node.req.url || '';
34 const method = event.node.req.method
35 const ip = getClientIP(event)
36 const userAgent = getRequestHeader(event, 'user-agent')
37 const startTime = Date.now()
38 // 🔹 只打印 /api 下的业务请求
39 const ignorePatterns = [
40 /^/_nuxt//,
41 /^/favicon.ico/,
42 /^/api/__nuxt/
43 ]
44 if (!url.startsWith('/api') || ignorePatterns.some(r => r.test(url))) return
45
46 // 🔹 获取请求体
47 let body = {}
48 try {
49 body = await readBody(event)
50 } catch {
51 }
52
53 const res = event.node.res
54 const oldWrite = res.write
55 const oldEnd = res.end
56 const chunks: Buffer[] = []
57
58 res.write = function (chunk: any, encoding?: string | ((err?: Error) => void), callback?: (err?: Error) => void) {
59 if (!(chunk instanceof Buffer)) chunk = Buffer.from(chunk)
60 chunks.push(chunk)
61 // @ts-ignore
62 return oldWrite.call(res, chunk, encoding as any, callback)
63 }
64
65 res.end = function (chunk?: any, encoding?: string | (() => void), callback?: () => void) {
66 if (chunk) {
67 if (!(chunk instanceof Buffer)) chunk = Buffer.from(chunk)
68 chunks.push(chunk)
69 }
70 const data = Buffer.concat(chunks).toString()
71 const time = formatDate(new Date())
72 const duration = Date.now() - startTime
73 const statusCode = res.statusCode
74 let methodColor = blue
75 if (method === 'POST') methodColor = green
76 if (method === 'PUT') methodColor = yellow
77 if (method === 'DELETE') methodColor = red
78 console.log(`${green}------------------- 请求日志:${time} -------------------${reset}`)
79 console.log(`[${magenta}${time}${reset}] ${methodColor}${method}${reset} ${cyan}${url}${reset} [${yellow}${statusCode}${reset}] (${duration}ms)`)
80 console.log(`[客户端] IP: ${ip}`)
81 console.log(`[UA] ${userAgent}`)
82 if (Object.keys(body).length) console.log(`[请求体] ${JSON.stringify(body, null, 2)}`)
83 console.log('返回值:')
84 // console.log(data)
85 console.log(`${green}------------------------------------------------------------------------
86${reset}`)
87 // @ts-ignore
88 return oldEnd.call(res, chunk, encoding as any, callback)
89 }
90})
91数据库相关操作
pg数据库
安装数据库
text
1pm add knex pg在server/utils目录下新建 db工具
text
1// server/utils/db.js
2import knex from 'knex'
3
4export const db = knex({
5 client: 'pg',
6 connection: {
7 host: '127.0.0.1',
8 port: 5432,
9 user: 'root',
10 password: 'root',
11 database: 'blog'
12 },
13 pool: {min: 0, max: 7} // 可选,连接池
14})
15在目录下就可以使用
text
1import {defineEventHandler} from 'h3'
2import {db} from '~~/server/utils/db'
3import type {UserDO} from "#shared/types";
4
5export default defineWrappedResponseHandler(defineEventHandler(async (event) => {
6 const config = useRuntimeConfig(event)
7 const cookies = parseCookies(event)
8 const users: UserDO[] = await db('users').select('*').where('id', '>', 1)
9 return users;
10}))redis
- nuxt.config.ts)[https://nuxt.com.cn/docs/4.x/guide/directory-structure/server]
nuxt.config.ts
1// https://nuxt.com/docs/api/configuration/nuxt-config
2import tailwindcss from "@tailwindcss/vite";
3
4export default defineNuxtConfig({
5 nitro: {
6 storage: {
7 redis: {
8 driver: 'redis',
9 /* redis 连接选项 */
10 port: 6379, // Redis 端口
11 host: "127.0.0.1", // Redis 主机
12 username: "", // 需要 Redis >= 6
13 password: "",
14 db: 0, // 默认值为 0
15 }
16 }
17 }
18})
19- 示例
text
1export default defineEventHandler(async (event) => {
2 // 列出所有键
3 const keys = await useStorage('redis').getKeys()
4
5 const v = await useStorage('redis').getItem("lx")
6 return {
7 hello: 'world ' + v, keys
8 }
9})
10异常统一操作
- server/utils/handler.ts
server/utils/handler.ts
1export const errorHandler = (code: number, msg: string) => {
2 return createError({
3 statusCode: 500,
4 statusMessage: '页面未找到',
5 data: {
6 myCustomField: true
7 }
8 })
9}
10示例
text
1export default defineEventHandler((event) => {
2 throw errorHandler(500, 'error')
3 return {
4 hello: 'world'
5 }
6})异常页面
text
1<template>
2 <div class="bg-gray-900 min-h-screen flex items-center justify-center overflow-hidden relative"
3 >
4 <!-- 粒子背景 -->
5 <div class="absolute inset-0">
6 <div class="particle" style="left: 10%; animation-delay: 0s;"></div>
7 <div class="particle" style="left: 20%; animation-delay: 1s;"></div>
8 <div class="particle" style="left: 30%; animation-delay: 2s;"></div>
9 <div class="particle" style="left: 40%; animation-delay: 3s;"></div>
10 <div class="particle" style="left: 50%; animation-delay: 4s;"></div>
11 <div class="particle" style="left: 60%; animation-delay: 5s;"></div>
12 <div class="particle" style="left: 70%; animation-delay: 0.5s;"></div>
13 <div class="particle" style="left: 80%; animation-delay: 1.5s;"></div>
14 <div class="particle" style="left: 90%; animation-delay: 2.5s;"></div>
15 </div>
16
17 <!-- 电路背景 -->
18 <svg class="absolute inset-0 w-full h-full opacity-20" viewBox="0 0 1000 1000">
19 <path class="circuit-line" stroke="#00ffff" stroke-width="1" fill="none"
20 d="M100,100 L200,100 L200,200 L400,200 L400,300 L600,300 L600,400 L800,400"/>
21 <path class="circuit-line" stroke="#ff00ff" stroke-width="1" fill="none"
22 d="M900,100 L800,100 L800,250 L600,250 L600,350 L400,350 L400,450 L200,450"
23 style="animation-delay: 1s;"/>
24 <path class="circuit-line" stroke="#ffff00" stroke-width="1" fill="none"
25 d="M100,900 L300,900 L300,700 L500,700 L500,500 L700,500 L700,300 L900,300"
26 style="animation-delay: 2s;"/>
27 </svg>
28
29 <div class="text-center z-10 relative px-20">
30 <!-- 脉冲环 -->
31 <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
32 <div class="pulse-ring w-32 h-32 border-2 border-cyan-400 rounded-full absolute"></div>
33 <div class="pulse-ring w-32 h-32 border-2 border-purple-400 rounded-full absolute"
34 style="animation-delay: 0.5s;"></div>
35 </div>
36
37 <!-- 主要内容 -->
38 <div class="float px-20">
39 <!-- 状态 数字 -->
40 <div class="tech-font text-8xl md:text-9xl font-black mb-8 relative">
41 <div
42 class="glitch neon-glow text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-purple-500 to-pink-500">
43 {{ statusCode }}
44 </div>
45 <div class="absolute top-0 left-0 glitch text-cyan-400 opacity-70" style="animation-delay: 0.1s;">
46 {{ statusCode }}
47 </div>
48 <div class="absolute top-0 left-0 glitch text-pink-400 opacity-50" style="animation-delay: 0.2s;">
49 {{ statusCode }}
50 </div>
51 </div>
52
53 <!-- 全息扫描效果 -->
54 <div class="hologram absolute inset-0 pointer-events-none"></div>
55
56 <!-- 错误信息 -->
57 <div class="tech-font text-xl md:text-2xl text-gray-300 mb-4 tracking-wider">
58 <span class="text-red-400">[ERROR]</span>
59 <span class="text-cyan-400">{{ statusMessage }}</span>
60 </div>
61
62 <div class="text-gray-400 mb-8 max-w-md mx-auto leading-relaxed">
63 <p class="mb-2">🚀 哎呀!看起来你进入了数字虚空</p>
64 <p class="text-sm opacity-75">这个页面可能被传送到了另一个维度...</p>
65 </div>
66
67 <!-- 系统状态 -->
68 <div
69 class="bg-gray-800 bg-opacity-50 border border-cyan-400 rounded-lg p-4 mb-8 max-w-sm mx-auto tech-font text-sm">
70 <div class="flex justify-between items-center mb-2">
71 <span class="text-gray-400">系统状态:</span>
72 <span class="text-green-400 animate-pulse">● 在线</span>
73 </div>
74 <div class="flex justify-between items-center mb-2">
75 <span class="text-gray-400">错误代码:</span>
76 <span class="text-red-400">0x{{ statusCode }}</span>
77 </div>
78 <div class="flex justify-between items-center">
79 <span class="text-gray-400">建议操作:</span>
80 <span class="text-yellow-400">返回主页</span>
81 </div>
82 </div>
83
84 <!-- 按钮组 -->
85 <div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
86 <button onclick="goHome()"
87 class="tech-font bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-600 hover:to-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-cyan-500/25">
88 🏠 返回主页
89 </button>
90
91 <button onclick="goBack()"
92 class="tech-font border-2 border-purple-500 text-purple-400 hover:bg-purple-500 hover:text-white px-8 py-3 rounded-lg font-semibold transition-all duration-300 transform hover:scale-105">
93 ← 返回上页
94 </button>
95 </div>
96
97 <!-- 有趣的提示 -->
98 <div class="mt-8 text-xs text-gray-500 tech-font">
99 <p class="animate-pulse">💡 小贴士: 试试按 Ctrl+Z 撤销这个错误?😄</p>
100 </div>
101 </div>
102 </div>
103
104 <!-- 角落装饰 -->
105 <div class="absolute top-4 left-4 w-16 h-16 border-l-2 border-t-2 border-cyan-400 opacity-50"></div>
106 <div class="absolute top-4 right-4 w-16 h-16 border-r-2 border-t-2 border-purple-400 opacity-50"></div>
107 <div class="absolute bottom-4 left-4 w-16 h-16 border-l-2 border-b-2 border-pink-400 opacity-50"></div>
108 <div class="absolute bottom-4 right-4 w-16 h-16 border-r-2 border-b-2 border-yellow-400 opacity-50"></div>
109 </div>
110</template>
111<script setup lang="ts">
112import type {NuxtError} from '#app'
113
114
115definePageMeta({
116 ssr: false
117})
118
119const props = defineProps({
120 error: Object as () => NuxtError
121})
122
123const statusCode = (props.error as NuxtError).statusCode;
124
125const statusMessage = (props.error as NuxtError).statusMessage || 'PAGE_NOT_FOUND';
126
127function goHome() {
128 // 添加一些科技感的过渡效果
129 document.body.style.transition = 'all 0.5s ease';
130 document.body.style.transform = 'scale(0.95)';
131 document.body.style.opacity = '0.7';
132
133 setTimeout(() => {
134 window.location.href = '/';
135 }, 500);
136}
137
138function goBack() {
139 document.body.style.transition = 'all 0.5s ease';
140 document.body.style.transform = 'translateX(-100%)';
141
142 setTimeout(() => {
143 window.history.back();
144 }, 500);
145}
146
147onMounted(() => {
148
149// 添加键盘交互
150 document.addEventListener('keydown', function (e) {
151 if (e.key === 'Enter') {
152 goHome();
153 } else if (e.key === 'Escape') {
154 goBack();
155 }
156 });
157
158// 鼠标跟随效果
159 document.addEventListener('mousemove', function (e) {
160 const cursor = document.createElement('div');
161 cursor.className = 'absolute w-1 h-1 bg-cyan-400 rounded-full pointer-events-none opacity-75';
162 cursor.style.left = e.clientX + 'px';
163 cursor.style.top = e.clientY + 'px';
164 cursor.style.animation = 'ping 1s cubic-bezier(0, 0, 0.2, 1) 1';
165 document.body.appendChild(cursor);
166
167 setTimeout(() => {
168 cursor.remove();
169 }, 1000);
170 });
171})
172</script>
173
174<style scoped>
175@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
176
177.tech-font {
178 font-family: 'Orbitron', monospace;
179}
180
181.glitch {
182 animation: glitch 2s infinite;
183}
184
185@keyframes glitch {
186 0%, 100% {
187 transform: translate(0);
188 }
189 10% {
190 transform: translate(-2px, -2px);
191 }
192 20% {
193 transform: translate(2px, 2px);
194 }
195 30% {
196 transform: translate(-2px, 2px);
197 }
198 40% {
199 transform: translate(2px, -2px);
200 }
201 50% {
202 transform: translate(-2px, -2px);
203 }
204 60% {
205 transform: translate(2px, 2px);
206 }
207 70% {
208 transform: translate(-2px, 2px);
209 }
210 80% {
211 transform: translate(2px, -2px);
212 }
213 90% {
214 transform: translate(-2px, -2px);
215 }
216}
217
218.float {
219 animation: float 3s ease-in-out infinite;
220}
221
222@keyframes float {
223 0%, 100% {
224 transform: translateY(0px);
225 }
226 50% {
227 transform: translateY(-20px);
228 }
229}
230
231.pulse-ring {
232 animation: pulse-ring 2s cubic-bezier(0.455, 0.03, 0.515, 0.955) infinite;
233}
234
235@keyframes pulse-ring {
236 0% {
237 transform: scale(0.33);
238 opacity: 1;
239 }
240 80%, 100% {
241 transform: scale(2.33);
242 opacity: 0;
243 }
244}
245
246.circuit-line {
247 stroke-dasharray: 1000;
248 stroke-dashoffset: 1000;
249 animation: draw 4s linear infinite;
250}
251
252@keyframes draw {
253 to {
254 stroke-dashoffset: 0;
255 }
256}
257
258.neon-glow {
259 text-shadow: 0 0 5px #00ffff,
260 0 0 10px #00ffff,
261 0 0 15px #00ffff,
262 0 0 20px #00ffff;
263}
264
265.particle {
266 position: absolute;
267 width: 2px;
268 height: 2px;
269 background: #00ffff;
270 border-radius: 50%;
271 animation: particle-float 6s linear infinite;
272}
273
274@keyframes particle-float {
275 0% {
276 transform: translateY(100vh) translateX(0);
277 opacity: 0;
278 }
279 10% {
280 opacity: 1;
281 }
282 90% {
283 opacity: 1;
284 }
285 100% {
286 transform: translateY(-100px) translateX(100px);
287 opacity: 0;
288 }
289}
290
291.hologram {
292 background: linear-gradient(45deg, transparent 30%, rgba(0, 255, 255, 0.1) 50%, transparent 70%);
293 background-size: 20px 20px;
294 animation: hologram-scan 3s linear infinite;
295}
296
297@keyframes hologram-scan {
298 0% {
299 background-position: 0 0;
300 }
301 100% {
302 background-position: 40px 40px;
303 }
304}
305</style>
306Nuxt 介绍
1. 文件目录
text
1components/* - 扩展默认组件
2composables/* - 扩展默认组合式函数
3layouts/* - 扩展默认布局
4pages/* - 扩展默认页面
5plugins/* - 扩展默认插件
6server/* - 扩展默认服务器端点和中间件
7utils/* - 扩展默认工具函数
8nuxt.config.ts - 扩展默认 Nuxt 配置
9app.config.ts - 扩展默认应用配置2. server
server 是一个 api 层。
简单路由
server/api/hello.ts
1# /api/query?foo=bar&baz=qux
2export default defineEventHandler((event) => {
3 const query = getQuery(event)
4 return { a: query.foo, b: query.baz }
5})布局开发
布局文件写在 /app/layouts
admin
1<template>
2 <div class="admin-layout">
3 <nav class="admin-nav">⚙️ Admin 导航</nav>
4 <section class="admin-content">
5 <slot />
6 </section>
7 </div>
8</template>blog
1<template>
2 <div class="blog-layout">
3 <aside class="sidebar">📚 Blog 菜单栏</aside>
4 <main class="content">
5 <slot />
6 </main>
7 </div>
8</template>app.vue
1<template>
2 <!-- Nuxt 自动根据页面的 definePageMeta(layout) 找对应布局 -->
3 <NuxtLayout>
4 <NuxtPage/>
5 </NuxtLayout>
6</template>
7middleware 动态匹配布局
layout.global.ts
1export default defineNuxtRouteMiddleware((to) => {
2 if (to.path.startsWith('/blog')) {
3 setPageLayout('blog')
4 } else if (to.path.startsWith('/admin')) {
5 setPageLayout('admin')
6 } else {
7 setPageLayout('default')
8 }
9})
10动态路由参数
server/api/hello/[name].ts
1export default defineEventHandler((event) => {
2 const name = getRouterParam(event, 'name')
3
4 return `Hello, ${name}!`
5})
6简单实例
1. 在构建时(SSG 静态生成)拉取本地数据
如果你的数据是本地文件(例如 JSON / YAML / Markdown),你可以在 server/api 或 composables 里直接读取,Nuxt 会在构建阶段就执行。
例子:读取本地 JSON
ts
1// server/api/data.get.ts
2import { readFileSync } from 'fs'
3import { join } from 'path'
4
5export default defineEventHandler(() => {
6 const filePath = join(process.cwd(), 'data/local.json')
7 const raw = readFileSync(filePath, 'utf-8')
8 return JSON.parse(raw)
9})页面使用:
vue
1<script setup lang="ts">
2const { data } = await useFetch('/api/data')
3</script>
4
5<template>
6 <pre>{{ data }}</pre>
7</template>👉 在 nuxt build && nuxt generate 时,Nuxt 会把数据拉取下来并生成静态页面。
2. 在 asyncData / useAsyncData 中获取本地数据
如果不想走 API,可以直接在页面里读本地数据:
vue
1<script setup lang="ts">
2import { readFileSync } from 'fs'
3import { join } from 'path'
4
5const { data } = await useAsyncData('local-data', () => {
6 const filePath = join(process.cwd(), 'data/local.json')
7 return JSON.parse(readFileSync(filePath, 'utf-8'))
8})
9</script>
10
11<template>
12 <div>{{ data }}</div>
13</template>⚠️ 注意:fs 只能在 服务端渲染阶段 使用,客户端不会执行这一段。
Nuxt 在 SSR 时会调用,客户端 hydration 只会复用服务端结果。
3. 如果是编译时固定数据(类似配置)
可以放在 runtimeConfig 或 app.config.ts,在编译时打包进去。
例子:
ts
1// app.config.ts
2export default defineAppConfig({
3 site: {
4 name: '我的站点',
5 items: ['a', 'b', 'c']
6 }
7})页面用:
vue
1<script setup lang="ts">
2const appConfig = useAppConfig()
3</script>
4
5<template>
6 <div>{{ appConfig.site.items }}</div>
7</template>这种适合不会变的“配置数据”。
4. 如果你想在构建时拉取本地/远程数据并生成静态页面
Nuxt3 提供 nitro prerender,你可以在 nuxt.config.ts 配置:
ts
1export default defineNuxtConfig({
2 nitro: {
3 prerender: {
4 routes: ['/about', '/products'] // 根据本地数据生成页面
5 }
6 }
7})配合 server/api 返回本地数据,就能在 nuxt generate 时提前生成静态页面。
✅ 总结:
- 本地 JSON / 文件数据 → 用
fs在server/api或useAsyncData中读。 - 配置类数据 → 用
app.config.ts或runtimeConfig。 - 需要生成静态 HTML → 配合
nitro prerender。