返回博客列表

Nuxt本地数据渲染

2026-01-29
11 min read
2025-08-30

集成 集成Arco 路径下回自动作为插件 引入tailwind 引入tailwind 统一ts类型 shared/types/index.d.ts Server api接口统一包装 - server/utils/handler.ts 统一请求日志 - server/middleware/log.ts 数据库相关操作 pg数据库 安装数据库 在server/utils目录下新建 db工具 在目录下...

集成

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

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

可以放在 runtimeConfigapp.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 / 文件数据 → 用 fsserver/apiuseAsyncData 中读。
  • 配置类数据 → 用 app.config.tsruntimeConfig
  • 需要生成静态 HTML → 配合 nitro prerender
返回博客列表
最后更新于 2026-01-29
想法或问题?在 GitHub Issue 下方参与讨论
去评论