项目概况

Nuxt本地数据渲染

2026-01-2911 min read未分类
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

引入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}); 14
app/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
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> 306

Nuxt 介绍

介绍

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> 7

middleware 动态匹配布局

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/apicomposables 里直接读取,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. 如果是编译时固定数据(类似配置)

可以放在 runtimeConfigapp.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 / 文件数据 → 用 fsserver/apiuseAsyncData 中读。
  • 配置类数据 → 用 app.config.tsruntimeConfig
  • 需要生成静态 HTML → 配合 nitro prerender