集成
集成Arco
- 路径下回自动作为插件
/app/plugins
import { defineNuxtPlugin } from '#app'
import ArcoVue from '@arco-design/web-vue'
import '@arco-design/web-vue/dist/arco.css'
import {Icon} from '@arco-design/web-vue';
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
const href = '//at.alicdn.com/t/c/font_4965719_1n8loeijakd.js';
/**
* 阿里图标
*/
export const IconFont = Icon.addFromIconFontCn({
src: `https://${href}`,
extraProps: {
// fill: 'white',
},
});
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(ArcoVue)
nuxtApp.vueApp.component('Iconfont', IconFont);
nuxtApp.vueApp.use(ArcoVueIcon);
})
引入tailwind
npm install tailwindcss @tailwindcss/vite
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
devtools: { enabled: true },
css: ['~/assets/css/tailwind.css'],
vite: {
plugins: [
tailwindcss(),
],
},
});
app/assets/css/tailwind.css
@import "tailwindcss";
统一ts类型
shared/types/index.d.ts
其他地方可以直接引用
import type {UserDO} from "#shared/types";
Server
api接口统一包装
- server/utils/handler.ts
server/utils/handler.ts
import type {EventHandler, EventHandlerRequest} from 'h3'
export const defineWrappedResponseHandler = <T extends EventHandlerRequest, D>(
handler: EventHandler<T, D>
): EventHandler<T, D> =>
defineEventHandler<T>(async event => {
try {
// 在路由处理器之前执行某些操作
const response = await handler(event)
// 在路由处理器之后执行某些操作
return {code: 0, data: response, msg: 'success'}
} catch (err) {
// 错误处理
return {code: -1, data: null, msg: err + ''}
}
})
统一请求日志
- server/middleware/log.ts
server/middleware/log.ts
import {defineEventHandler, getRequestURL, getRequestHeader, readBody} from 'h3'
// ANSI 颜色码
const reset = "x1b[0m"
const red = "x1b[31m"
const green = "x1b[32m"
const yellow = "x1b[33m"
const blue = "x1b[34m"
const magenta = "x1b[35m"
const cyan = "x1b[36m"
const gray = "x1b[90m"
function formatDate(date: Date) {
const pad = (n: number, width = 2) => n.toString().padStart(width, '0')
const year = date.getFullYear()
const month = pad(date.getMonth() + 1)
const day = pad(date.getDate())
const hour = pad(date.getHours())
const minute = pad(date.getMinutes())
const second = pad(date.getSeconds())
const ms = date.getMilliseconds().toString().padStart(3, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}.${ms}`
}
function getClientIP(event: any) {
const forwarded = event.node.req.headers['x-forwarded-for'] as string
if (forwarded) return forwarded.split(',')[0].trim() // 取第一个真实 IP
const ip = event.node.req.socket?.remoteAddress || ''
return ip.replace('::ffff:', '') // IPv6 映射 IPv4 时去掉前缀
}
export default defineEventHandler(async (event) => {
const url = event.node.req.url || '';
const method = event.node.req.method
const ip = getClientIP(event)
const userAgent = getRequestHeader(event, 'user-agent')
const startTime = Date.now()
// 🔹 只打印 /api 下的业务请求
const ignorePatterns = [
/^/_nuxt//,
/^/favicon.ico/,
/^/api/__nuxt/
]
if (!url.startsWith('/api') || ignorePatterns.some(r => r.test(url))) return
// 🔹 获取请求体
let body = {}
try {
body = await readBody(event)
} catch {
}
const res = event.node.res
const oldWrite = res.write
const oldEnd = res.end
const chunks: Buffer[] = []
res.write = function (chunk: any, encoding?: string | ((err?: Error) => void), callback?: (err?: Error) => void) {
if (!(chunk instanceof Buffer)) chunk = Buffer.from(chunk)
chunks.push(chunk)
// @ts-ignore
return oldWrite.call(res, chunk, encoding as any, callback)
}
res.end = function (chunk?: any, encoding?: string | (() => void), callback?: () => void) {
if (chunk) {
if (!(chunk instanceof Buffer)) chunk = Buffer.from(chunk)
chunks.push(chunk)
}
const data = Buffer.concat(chunks).toString()
const time = formatDate(new Date())
const duration = Date.now() - startTime
const statusCode = res.statusCode
let methodColor = blue
if (method === 'POST') methodColor = green
if (method === 'PUT') methodColor = yellow
if (method === 'DELETE') methodColor = red
console.log(`${green}------------------- 请求日志:${time} -------------------${reset}`)
console.log(`[${magenta}${time}${reset}] ${methodColor}${method}${reset} ${cyan}${url}${reset} [${yellow}${statusCode}${reset}] (${duration}ms)`)
console.log(`[客户端] IP: ${ip}`)
console.log(`[UA] ${userAgent}`)
if (Object.keys(body).length) console.log(`[请求体] ${JSON.stringify(body, null, 2)}`)
console.log('返回值:')
// console.log(data)
console.log(`${green}------------------------------------------------------------------------
${reset}`)
// @ts-ignore
return oldEnd.call(res, chunk, encoding as any, callback)
}
})
数据库相关操作
pg数据库
安装数据库
pm add knex pg
在server/utils目录下新建 db工具
// server/utils/db.js
import knex from 'knex'
export const db = knex({
client: 'pg',
connection: {
host: '127.0.0.1',
port: 5432,
user: 'root',
password: 'root',
database: 'blog'
},
pool: {min: 0, max: 7} // 可选,连接池
})
在目录下就可以使用
import {defineEventHandler} from 'h3'
import {db} from '~~/server/utils/db'
import type {UserDO} from "#shared/types";
export default defineWrappedResponseHandler(defineEventHandler(async (event) => {
const config = useRuntimeConfig(event)
const cookies = parseCookies(event)
const users: UserDO[] = await db('users').select('*').where('id', '>', 1)
return users;
}))
redis
- nuxt.config.ts)[https://nuxt.com.cn/docs/4.x/guide/directory-structure/server]
nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
nitro: {
storage: {
redis: {
driver: 'redis',
/* redis 连接选项 */
port: 6379, // Redis 端口
host: "127.0.0.1", // Redis 主机
username: "", // 需要 Redis >= 6
password: "",
db: 0, // 默认值为 0
}
}
}
})
- 示例
export default defineEventHandler(async (event) => {
// 列出所有键
const keys = await useStorage('redis').getKeys()
const v = await useStorage('redis').getItem("lx")
return {
hello: 'world ' + v, keys
}
})
异常统一操作
- server/utils/handler.ts
server/utils/handler.ts
export const errorHandler = (code: number, msg: string) => {
return createError({
statusCode: 500,
statusMessage: '页面未找到',
data: {
myCustomField: true
}
})
}
示例
export default defineEventHandler((event) => {
throw errorHandler(500, 'error')
return {
hello: 'world'
}
})
异常页面
<template>
<div class="bg-gray-900 min-h-screen flex items-center justify-center overflow-hidden relative"
>
<!-- 粒子背景 -->
<div class="absolute inset-0">
<div class="particle" style="left: 10%; animation-delay: 0s;"></div>
<div class="particle" style="left: 20%; animation-delay: 1s;"></div>
<div class="particle" style="left: 30%; animation-delay: 2s;"></div>
<div class="particle" style="left: 40%; animation-delay: 3s;"></div>
<div class="particle" style="left: 50%; animation-delay: 4s;"></div>
<div class="particle" style="left: 60%; animation-delay: 5s;"></div>
<div class="particle" style="left: 70%; animation-delay: 0.5s;"></div>
<div class="particle" style="left: 80%; animation-delay: 1.5s;"></div>
<div class="particle" style="left: 90%; animation-delay: 2.5s;"></div>
</div>
<!-- 电路背景 -->
<svg class="absolute inset-0 w-full h-full opacity-20" viewBox="0 0 1000 1000">
<path class="circuit-line" stroke="#00ffff" stroke-width="1" fill="none"
d="M100,100 L200,100 L200,200 L400,200 L400,300 L600,300 L600,400 L800,400"/>
<path class="circuit-line" stroke="#ff00ff" stroke-width="1" fill="none"
d="M900,100 L800,100 L800,250 L600,250 L600,350 L400,350 L400,450 L200,450"
style="animation-delay: 1s;"/>
<path class="circuit-line" stroke="#ffff00" stroke-width="1" fill="none"
d="M100,900 L300,900 L300,700 L500,700 L500,500 L700,500 L700,300 L900,300"
style="animation-delay: 2s;"/>
</svg>
<div class="text-center z-10 relative px-20">
<!-- 脉冲环 -->
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div class="pulse-ring w-32 h-32 border-2 border-cyan-400 rounded-full absolute"></div>
<div class="pulse-ring w-32 h-32 border-2 border-purple-400 rounded-full absolute"
style="animation-delay: 0.5s;"></div>
</div>
<!-- 主要内容 -->
<div class="float px-20">
<!-- 状态 数字 -->
<div class="tech-font text-8xl md:text-9xl font-black mb-8 relative">
<div
class="glitch neon-glow text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-purple-500 to-pink-500">
{{ statusCode }}
</div>
<div class="absolute top-0 left-0 glitch text-cyan-400 opacity-70" style="animation-delay: 0.1s;">
{{ statusCode }}
</div>
<div class="absolute top-0 left-0 glitch text-pink-400 opacity-50" style="animation-delay: 0.2s;">
{{ statusCode }}
</div>
</div>
<!-- 全息扫描效果 -->
<div class="hologram absolute inset-0 pointer-events-none"></div>
<!-- 错误信息 -->
<div class="tech-font text-xl md:text-2xl text-gray-300 mb-4 tracking-wider">
<span class="text-red-400">[ERROR]</span>
<span class="text-cyan-400">{{ statusMessage }}</span>
</div>
<div class="text-gray-400 mb-8 max-w-md mx-auto leading-relaxed">
<p class="mb-2">🚀 哎呀!看起来你进入了数字虚空</p>
<p class="text-sm opacity-75">这个页面可能被传送到了另一个维度...</p>
</div>
<!-- 系统状态 -->
<div
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">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-400">系统状态:</span>
<span class="text-green-400 animate-pulse">● 在线</span>
</div>
<div class="flex justify-between items-center mb-2">
<span class="text-gray-400">错误代码:</span>
<span class="text-red-400">0x{{ statusCode }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-400">建议操作:</span>
<span class="text-yellow-400">返回主页</span>
</div>
</div>
<!-- 按钮组 -->
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
<button onclick="goHome()"
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">
🏠 返回主页
</button>
<button onclick="goBack()"
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">
← 返回上页
</button>
</div>
<!-- 有趣的提示 -->
<div class="mt-8 text-xs text-gray-500 tech-font">
<p class="animate-pulse">💡 小贴士: 试试按 Ctrl+Z 撤销这个错误?😄</p>
</div>
</div>
</div>
<!-- 角落装饰 -->
<div class="absolute top-4 left-4 w-16 h-16 border-l-2 border-t-2 border-cyan-400 opacity-50"></div>
<div class="absolute top-4 right-4 w-16 h-16 border-r-2 border-t-2 border-purple-400 opacity-50"></div>
<div class="absolute bottom-4 left-4 w-16 h-16 border-l-2 border-b-2 border-pink-400 opacity-50"></div>
<div class="absolute bottom-4 right-4 w-16 h-16 border-r-2 border-b-2 border-yellow-400 opacity-50"></div>
</div>
</template>
<script setup lang="ts">
import type {NuxtError} from '#app'
definePageMeta({
ssr: false
})
const props = defineProps({
error: Object as () => NuxtError
})
const statusCode = (props.error as NuxtError).statusCode;
const statusMessage = (props.error as NuxtError).statusMessage || 'PAGE_NOT_FOUND';
function goHome() {
// 添加一些科技感的过渡效果
document.body.style.transition = 'all 0.5s ease';
document.body.style.transform = 'scale(0.95)';
document.body.style.opacity = '0.7';
setTimeout(() => {
window.location.href = '/';
}, 500);
}
function goBack() {
document.body.style.transition = 'all 0.5s ease';
document.body.style.transform = 'translateX(-100%)';
setTimeout(() => {
window.history.back();
}, 500);
}
onMounted(() => {
// 添加键盘交互
document.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
goHome();
} else if (e.key === 'Escape') {
goBack();
}
});
// 鼠标跟随效果
document.addEventListener('mousemove', function (e) {
const cursor = document.createElement('div');
cursor.className = 'absolute w-1 h-1 bg-cyan-400 rounded-full pointer-events-none opacity-75';
cursor.style.left = e.clientX + 'px';
cursor.style.top = e.clientY + 'px';
cursor.style.animation = 'ping 1s cubic-bezier(0, 0, 0.2, 1) 1';
document.body.appendChild(cursor);
setTimeout(() => {
cursor.remove();
}, 1000);
});
})
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
.tech-font {
font-family: 'Orbitron', monospace;
}
.glitch {
animation: glitch 2s infinite;
}
@keyframes glitch {
0%, 100% {
transform: translate(0);
}
10% {
transform: translate(-2px, -2px);
}
20% {
transform: translate(2px, 2px);
}
30% {
transform: translate(-2px, 2px);
}
40% {
transform: translate(2px, -2px);
}
50% {
transform: translate(-2px, -2px);
}
60% {
transform: translate(2px, 2px);
}
70% {
transform: translate(-2px, 2px);
}
80% {
transform: translate(2px, -2px);
}
90% {
transform: translate(-2px, -2px);
}
}
.float {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
}
.pulse-ring {
animation: pulse-ring 2s cubic-bezier(0.455, 0.03, 0.515, 0.955) infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(0.33);
opacity: 1;
}
80%, 100% {
transform: scale(2.33);
opacity: 0;
}
}
.circuit-line {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 4s linear infinite;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
.neon-glow {
text-shadow: 0 0 5px #00ffff,
0 0 10px #00ffff,
0 0 15px #00ffff,
0 0 20px #00ffff;
}
.particle {
position: absolute;
width: 2px;
height: 2px;
background: #00ffff;
border-radius: 50%;
animation: particle-float 6s linear infinite;
}
@keyframes particle-float {
0% {
transform: translateY(100vh) translateX(0);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(-100px) translateX(100px);
opacity: 0;
}
}
.hologram {
background: linear-gradient(45deg, transparent 30%, rgba(0, 255, 255, 0.1) 50%, transparent 70%);
background-size: 20px 20px;
animation: hologram-scan 3s linear infinite;
}
@keyframes hologram-scan {
0% {
background-position: 0 0;
}
100% {
background-position: 40px 40px;
}
}
</style>
Nuxt 介绍
1. 文件目录
components/* - 扩展默认组件
composables/* - 扩展默认组合式函数
layouts/* - 扩展默认布局
pages/* - 扩展默认页面
plugins/* - 扩展默认插件
server/* - 扩展默认服务器端点和中间件
utils/* - 扩展默认工具函数
nuxt.config.ts - 扩展默认 Nuxt 配置
app.config.ts - 扩展默认应用配置
2. server
server 是一个 api 层。
简单路由
server/api/hello.ts
# /api/query?foo=bar&baz=qux
export default defineEventHandler((event) => {
const query = getQuery(event)
return { a: query.foo, b: query.baz }
})
布局开发
布局文件写在 /app/layouts
admin
<template>
<div class="admin-layout">
<nav class="admin-nav">⚙️ Admin 导航</nav>
<section class="admin-content">
<slot />
</section>
</div>
</template>
blog
<template>
<div class="blog-layout">
<aside class="sidebar">📚 Blog 菜单栏</aside>
<main class="content">
<slot />
</main>
</div>
</template>
app.vue
<template>
<!-- Nuxt 自动根据页面的 definePageMeta(layout) 找对应布局 -->
<NuxtLayout>
<NuxtPage/>
</NuxtLayout>
</template>
middleware 动态匹配布局
layout.global.ts
export default defineNuxtRouteMiddleware((to) => {
if (to.path.startsWith('/blog')) {
setPageLayout('blog')
} else if (to.path.startsWith('/admin')) {
setPageLayout('admin')
} else {
setPageLayout('default')
}
})
动态路由参数
server/api/hello/[name].ts
export default defineEventHandler((event) => {
const name = getRouterParam(event, 'name')
return `Hello, ${name}!`
})
简单实例
1. 在构建时(SSG 静态生成)拉取本地数据
如果你的数据是本地文件(例如 JSON / YAML / Markdown),你可以在 server/api 或 composables 里直接读取,Nuxt 会在构建阶段就执行。
例子:读取本地 JSON
ts
// server/api/data.get.ts
import { readFileSync } from 'fs'
import { join } from 'path'
export default defineEventHandler(() => {
const filePath = join(process.cwd(), 'data/local.json')
const raw = readFileSync(filePath, 'utf-8')
return JSON.parse(raw)
})
页面使用:
vue
<script setup lang="ts">
const { data } = await useFetch('/api/data')
</script>
<template>
<pre>{{ data }}</pre>
</template>
👉 在 nuxt build && nuxt generate 时,Nuxt 会把数据拉取下来并生成静态页面。
2. 在 asyncData / useAsyncData 中获取本地数据
如果不想走 API,可以直接在页面里读本地数据:
vue
<script setup lang="ts">
import { readFileSync } from 'fs'
import { join } from 'path'
const { data } = await useAsyncData('local-data', () => {
const filePath = join(process.cwd(), 'data/local.json')
return JSON.parse(readFileSync(filePath, 'utf-8'))
})
</script>
<template>
<div>{{ data }}</div>
</template>
⚠️ 注意:fs 只能在 服务端渲染阶段 使用,客户端不会执行这一段。
Nuxt 在 SSR 时会调用,客户端 hydration 只会复用服务端结果。
3. 如果是编译时固定数据(类似配置)
可以放在 runtimeConfig 或 app.config.ts,在编译时打包进去。
例子:
ts
// app.config.ts
export default defineAppConfig({
site: {
name: '我的站点',
items: ['a', 'b', 'c']
}
})
页面用:
vue
<script setup lang="ts">
const appConfig = useAppConfig()
</script>
<template>
<div>{{ appConfig.site.items }}</div>
</template>
这种适合不会变的“配置数据”。
4. 如果你想在构建时拉取本地/远程数据并生成静态页面
Nuxt3 提供 nitro prerender,你可以在 nuxt.config.ts 配置:
ts
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/about', '/products'] // 根据本地数据生成页面
}
}
})
配合 server/api 返回本地数据,就能在 nuxt generate 时提前生成静态页面。
✅ 总结:
- 本地 JSON / 文件数据 → 用
fs在server/api或useAsyncData中读。 - 配置类数据 → 用
app.config.ts或runtimeConfig。 - 需要生成静态 HTML → 配合
nitro prerender。
想法或问题?在 GitHub Issue 下方参与讨论
去评论