从零搭建企业官网:Nuxt3 SSR + Prisma 全栈实践

在当今数字化时代,企业官网不仅是品牌展示的窗口,更是业务增长的重要渠道。本文将带你使用现代技术栈从零搭建一个高性能、SEO友好的企业官网,结合Nuxt3的SSR能力和Prisma的强大数据库管理功能。

1. 项目初始化与技术选型

我们采用以下技术栈构建全栈解决方案:

  • 前端框架:Nuxt3(基于Vue3),提供开箱即用的SSR能力
  • 后端接口:Nuxt3 Server API + Prisma
  • 数据库:MySQL(通过Prisma管理)
  • 样式方案:TailwindCSS(实用优先的CSS框架)
  • 状态管理:Pinia(轻量且强大的Vue状态管理库)
  • SEO优化:useHead、meta标签结合SSR渲染

项目初始化步骤

npx nuxi init nuxt3-enterprise-website
cd nuxt3-enterprise-website
npm install
npm install pinia @pinia/nuxt prisma @prisma/client tailwindcss postcss autoprefixer
npx prisma init

详细package.json

{
  "name": "nuxt3-enterprise-website",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "env-cmd -f .env.development nuxt dev",
    "dev:test": "env-cmd -f .env.test nuxt dev",
    "dev:prod": "env-cmd -f .env.production nuxt dev",
    "build:test": "NODE_ENV=test nuxt build",
    "build:dev": "NODE_ENV=development nuxt build",
    "build:prod": "NODE_ENV=production nuxt build",
    "preview:test": "NODE_ENV=test nuxt preview",
    "preview:dev": "NODE_ENV=development nuxt preview",
    "preview:prod": "NODE_ENV=production nuxt preview",
    "start:test": "NODE_ENV=test node .output/server/index.mjs",
    "start:prod": "NODE_ENV=production node .output/server/index.mjs"
  },
  "dependencies": {
    "@nuxtjs/color-mode": "^3.5.2",
    "@nuxtjs/tailwindcss": "7.0.0-beta.0",
    "@pinia/nuxt": "^0.11.0",
    "@vueuse/head": "^2.0.0",
    "argon2": "^0.43.0",
    "axios": "^1.9.0",
    "dayjs": "^1.11.13",
    "jsonwebtoken": "^9.0.2",
    "mysql2": "^3.14.1",
    "nanoid": "^5.1.5",
    "nuxt": "^3.17.4",
    "pinia": "^3.0.2",
    "vue": "^3.5.15",
    "vue-router": "^4.5.1"
  },
  "devDependencies": {
    "@nuxtjs/tailwindcss": "^6.8.0",
    "@types/jsonwebtoken": "^9.0.9",
    "@types/node": "^22.15.29",
    "autoprefixer": "^10.4.21",
    "env-cmd": "^10.1.0",
    "postcss": "^8.5.4",
    "tailwindcss": "^4.1.8",
    "vue-tsc": "^2.2.10"
  }
}

2. 项目结构说明

项目采用模块化结构设计,职责清晰:

├── app.vue                 # 根组件
├── nuxt.config.ts          # Nuxt配置
├── components/             # 复用组件
│   ├── sections/           # 业务区块组件
│   ├── AppHeader.vue       # 网站头部
│   ├── Footer.vue          # 网站底部
├── composables/            # 组合式函数
│   ├── useApi.ts           # API请求封装
│   ├── useUserApi.ts       # 用户相关API
├── pages/                  # 路由页面
│   ├── index.vue           # 首页
│   ├── about.vue           # 关于我们
│   ├── products/           # 产品列表页
│   │   └── index.vue
├── server/                 # 后端接口
│   ├── api/                # API路由
│   │   ├── users.ts        # 用户接口
│   │   ├── page.ts         # 页面数据接口
│   ├── middleware/         # 全局中间件
│   │   └── auth.ts         # 鉴权中间件
├── prisma/                 # Prisma配置
│   └── schema.prisma       # 数据模型定义
└── utils/                  # 工具函数
    └── storage.ts          # 本地存储封装

3. Nuxt3 SSR配置与SPA Loading

nuxt.config.ts中开启SSR并配置

export default defineNuxtConfig({
  ssr: true, // 开启服务器端渲染
  app: { 
    rootId: 'nuxt-app',       // 确保根容器 ID 明确
    head: {
      charset: 'utf-8',
      viewport: 'width=device-width, initial-scale=1',
      title: '企业官网标题',
      meta: [
        { name: 'description', content: '企业官网描述1' },
        { name: 'keywords', content: '关键词1,关键词2' },
        // Open Graph 协议
        { property: 'og:type', content: 'website' },
        { property: 'og:title', content: '企业官网标题' },
        { property: 'og:description', content: '企业官网描述' },
        { property: 'og:image', content: 'https://你的域名.com/share-image.jpg' },
        // Twitter 卡片
        { name: 'twitter:card', content: 'summary_large_image' }
      ],
      link: [
        // 修正 canonical 配置方式
        { rel: 'canonical', href: 'https://你的域名.com' }
      ],
    }
  },
  // 启用 TypeScript
  typescript: {
    strict: true,
    typeCheck: true
  },	
  components: [
    { path: '~/components', pathPrefix: false } // 禁用路径前缀
  ],
  // 模块配置
  modules: [
    '@pinia/nuxt',
    '@nuxtjs/tailwindcss'
  ],
   // 自动导入配置
  imports: {
    dirs: ['stores']
  }
})

4. 前端页面与组件开发

页面路由

Nuxt3基于文件系统自动生成路由:

  • pages/index.vue → 首页(/
  • pages/about.vue → 关于我们页(/about
  • pages/products/index.vue → 产品列表页(/products

页面结构示例(首页)

<template>
  <div class="relative">
    <FloorNavigation @navigate="navigateTo" />
    <section id="home">
      <HeroSection />
    </section>
    <section id="about" class="py-20 bg-white">
      <AboutSection />
    </section>
    <section id="services" class="py-20 bg-gray-50">
      <ServicesSection />
    </section>
    <section id="products" class="py-20 bg-white">
      <ProductsSection />
    </section>
    <section id="news" class="py-20 bg-gray-50">
      <NewsSection />
    </section>
    <section id="contact" class="py-20 bg-indigo-700 text-white">
      <ContactSection />
    </section>
  </div>
</template>
<script setup lang="ts">
import HeroSection from '../components/sections/HeroSection.vue'
import AboutSection from '../components/sections/AboutSection.vue'
import ServicesSection from '../components/sections/ServicesSection.vue'
import ProductsSection from '../components/sections/ProductsSection.vue'
import NewsSection from '../components/sections/NewsSection.vue'
import ContactSection from '../components/sections/ContactSection.vue'
import FloorNavigation from '../components/FloorNavigation.vue'
// 使用useHead来设置页面的SEO信息
useSeo({
  title: '首页 - 创新驱动未来',
  description: '数字化解决方案, 企业服务, 创新科技',
  keywords: '数字化, 企业服务, 创新科技, 产品服务, 联系我们',
  image: 'https://www.example.com/logo.png',
})

const navigateTo = (id: string) => {
  const element = document.getElementById(id)
  if (element) {
    element.scrollIntoView({ behavior: 'smooth' })
  }
}

</script>

响应式组件示例(产品卡片)

<template>
  <div
    class="relative h-screen flex items-center justify-center bg-gradient-to-br from-indigo-900 to-purple-800 text-white">
    <div class="container mx-auto px-6 text-center">
      <h1 class="text-4xl md:text-6xl font-bold mb-6 animate-fade-in">
        创新驱动未来
      </h1>
      <p class="text-xl md:text-2xl mb-10 max-w-2xl mx-auto animate-fade-in delay-100">
        我们致力于为企业提供最先进的数字化解决方案
      </p>
      <div class="flex flex-col sm:flex-row justify-center gap-4 animate-fade-in delay-200">
        <button class="px-8 py-3 bg-white text-indigo-600 font-medium rounded-lg hover:bg-gray-100 transition-colors">
          产品服务
        </button>
        <button
          class="px-8 py-3 border-2 border-white text-white font-medium rounded-lg hover:bg-indigo hover:bg-opacity-10 transition-colors">
          联系我们
        </button>
      </div>
    </div>

    <button @click="scrollToNext"
      class="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce text-white hover:text-indigo-200 transition-colors"
      aria-label="向下滚动">
      <!-- <ArrowDownIcon class="h-8 w-8" /> -->
      向下滚动
    </button>
  </div>
</template>
<script setup lang="ts">
// import { ArrowDownIcon } from '@heroicons/vue/24/outline'

const scrollToNext = () => {
  const element = document.getElementById('about')
  if (element) {
    element.scrollIntoView({ behavior: 'smooth' })
  }
}
</script>```

响应式布局:使用TailwindCSS的响应式工具类:

<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
  <!-- 产品卡片 -->
</div>
## 5. 后端接口与数据库

### Prisma数据模型

```prisma
// prisma/schema.prisma
model AboutUs {
  id          Int                @id @default(autoincrement())
  sectionType AboutUsSectionType
  title       String?            @db.VarChar(100)
  content     String             @db.Text
  year        String?            @db.VarChar(10)
  iconClass   String?            @db.VarChar(50)
  bgColor     String?            @db.VarChar(50)
  textColor   String?            @db.VarChar(50)
  sortOrder   Int?              @default(0)
  createdAt   DateTime          @default(now()) @db.Timestamp(0)
  updatedAt   DateTime          @updatedAt @db.Timestamp(0)

  @@map("about_us")
}

###数据库创建,造数据示例

CREATE TABLE IF NOT EXISTS about_us (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    section_type ENUM('intro', 'vision_mission', 'milestone', 'culture') NOT NULL,
    title VARCHAR(100),
    content TEXT NOT NULL,
    year VARCHAR(10) COMMENT '仅用于发展历程年份',
    icon_class VARCHAR(50) COMMENT '图标或样式类名',
    bg_color VARCHAR(50) COMMENT '背景颜色类',
    text_color VARCHAR(50) COMMENT '文字颜色类',
    sort_order TINYINT UNSIGNED DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_section_type (section_type),
    INDEX idx_sort (section_type, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO about_us (section_type, title, content, year, icon_class, bg_color, text_color, sort_order) VALUES
('intro', NULL, '我们是一家致力于为企业提供数字化转型、系统开发与智能营销解决方案的高新技术公司...', NULL, NULL, NULL, NULL, 1),

('vision_mission', '企业愿景', '成为中国领先的企业数字化服务提供商', NULL, 'i-carbon-idea', 'bg-indigo-50', 'text-indigo-600', 1),
('vision_mission', '企业使命', '用科技创新驱动企业成长与社会进步', NULL, 'i-carbon-mission', 'bg-purple-50', 'text-purple-600', 2),

('milestone', '公司成立', '专注企业数字化咨询', '2018', 'i-carbon-circle-solid', 'bg-indigo-400', 'text-indigo-600', 1),
('milestone', '智能营销平台', '服务客户突破百家', '2020', 'i-carbon-circle-solid', 'bg-purple-400', 'text-purple-600', 2),
('milestone', '行业荣誉', '团队规模持续壮大', '2023', 'i-carbon-circle-solid', 'bg-blue-400', 'text-blue-600', 3),

('culture', '创新', '持续技术创新,追求卓越', NULL, 'i-carbon-innovation', 'bg-white', 'text-indigo-500', 1),
('culture', '协作', '团队合作,共创价值', NULL, 'i-carbon-collaborate', 'bg-white', 'text-indigo-500', 2),
('culture', '诚信', '以诚为本,信守承诺', NULL, 'i-carbon-user-certification', 'bg-white', 'text-indigo-500', 3),
('culture', '共赢', '客户至上,合作共赢', NULL, 'i-carbon-partnership', 'bg-white', 'text-indigo-500', 4);

连接数据库(server/utils/db.ts

import mysql from 'mysql2'


// 修改后的QueryResult接口
interface QueryResult<T = any> {
  results: T extends mysql.RowDataPacket[] ? T :
  T extends mysql.OkPacket ? [T] :
  any[]  // 最终回退到any[]确保兼容
  fields?: mysql.FieldPacket[]
}

const pool = mysql.createPool({
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'root',
  password: process.env.DB_PASSWORD || '12345678',
  database: process.env.DB_NAME || 'moten',
  port: parseInt(process.env.DB_PORT || '3306'),
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
})

const promisePool = pool.promise()

// 修改后的查询方法(添加类型断言)
export const query = async <T = any>(sql: string, values?: any[]): Promise<QueryResult<T>> => {
  const [result, fields] = await promisePool.query(sql, values)

  return {
    results: (Array.isArray(result) ? result : [result]) as T extends mysql.RowDataPacket[] ? T :
      T extends mysql.OkPacket ? [T] : any[],
    fields
  }
}

// 执行方法保持不变
export const execute = async (sql: string, values?: any[]): Promise<mysql.OkPacket> => {
  const [result] = await promisePool.query<mysql.OkPacket>(sql, values)
  return result
}

// 错误处理封装
export async function daoErrorHandler<T>(fn: () => Promise<T>): Promise<T> {
  try {
    return await fn()
  } catch (error: any) {
    console.error('Database error:', error)
    throw createError({
      statusCode: 500,
      message: 'Database operation failed',
      data: {
        code: error.code,
        sqlMessage: error.sqlMessage
      }
    })
  }
}

export default pool

用户管理API(server/api/users.ts)

import { defineEventHandler, getQuery, readBody } from 'h3'
import { prisma } from '../utils/prisma'
import { validateUserUpdate } from '../validators/user'

export default defineEventHandler(async (event) => {
  const method = event.node.req.method

  // 获取用户列表
  if (method === 'GET') {
    const { page = 1, size = 10 } = getQuery(event)
    const skip = (Number(page) - 1) * Number(size)
    
    const [users, total] = await Promise.all([
      prisma.user.findMany({
        skip,
        take: Number(size),
        select: { id: true, username: true, role: true }
      }),
      prisma.user.count()
    ])
    
    return {
      data: users,
      pagination: {
        total,
        currentPage: Number(page),
        perPage: Number(size),
        totalPages: Math.ceil(total / Number(size))
      }
    }
  }

  // 更新用户信息
  if (method === 'PUT') {
    const body = await readBody(event)
    const { error } = validateUserUpdate(body)
    if (error) throw createError({ statusCode: 400, message: error.message })
    
    const updatedUser = await prisma.user.update({
      where: { id: body.id },
      data: {
        username: body.username,
        role: body.role
      }
    })
    
    return { data: updatedUser }
  }
})

鉴权中间件(server/middleware/auth.ts)

import jwt from 'jsonwebtoken'
const { verify } = jwt
import { sendErrorResponse, ResponseCode } from '../utils/response'

const jwtSecret = process.env.JWT_SECRET || 'your-secret-key'

interface JwtPayloadWithRole extends jwt.JwtPayload {
  role: string
}

const NO_AUTH_ROUTES = [  // 需要排除的接口
  '/api/login',
  '/api/register',
  '/api/page-data',
  '/api/contact',
  '/api/about-us',
]

export default defineEventHandler(async (event) => {
  // 1. 检查是否在白名单
  if (NO_AUTH_ROUTES.some(route => event.path!.startsWith(route))) {
    return
  }
  if (!event.path?.startsWith('/api')) {
    return
  }
  // Skip auth routes
  if (event.path?.startsWith('/api/auth')) {
    return
  }

  // 2. 检查是否包含token
  const authHeader = getHeader(event, 'Authorization')
  const token = authHeader?.startsWith('Bearer ')
    ? authHeader.split(' ')[1]
    : authHeader || getCookie(event, 'auth_token')

  console.log('Token:', token) // Debug

  if (!token) {
    // 对于API请求返回JSON错误
    if (event.path?.startsWith('/api')) {
      return sendErrorResponse(
        event,
        ResponseCode.UNAUTHORIZED,
        'Authentication required'
      )
    }
    // 对于页面请求重定向到登录页
    return sendRedirect(event, '/login')
  }

  try {
    const decoded = verify(token, jwtSecret) as JwtPayloadWithRole

    event.context.user = decoded

    // 3. 检查权限
    if (decoded?.role !== 'admin' && event.path?.startsWith('/api/admin')) {
      return sendErrorResponse(
        event,
        ResponseCode.FORBIDDEN,
        'Insufficient permissions'
      )
    }
  } catch (err) {
    console.error('JWT Verification Failed:', err) // 打印具体错误
    return sendErrorResponse(
      event,
      ResponseCode.UNAUTHORIZED,
      'Invalid or expired token'
    )
  }
})

6. SEO优化实践

全局SEO配置(nuxt.config.ts)

export default defineNuxtConfig({
  app: {
    head: {
      title: '企业官网',
      meta: [
        { name: 'description', content: '专业的企业官网解决方案' },
        { name: 'og:image', content: '/og-image.jpg' },
        { name: 'twitter:card', content: 'summary_large_image' }
      ],
      link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
    }
  }
})

页面级SEO(about.vue)

<script setup>
useHead({
  title: '关于我们',
  meta: [
    { name: 'description', content: '了解公司发展历程和团队' },
    { property: 'og:image', content: '/about-og.jpg' }
  ],
  script: [{ 
    innerHTML: JSON.stringify({
      "@context": "https://schema.org",
      "@type": "Organization",
      "name": "企业名称",
      "url": "https://example.com",
      "logo": "https://example.com/logo.png"
    }),
    type: 'application/ld+json'
  }]
})

// 获取关于页面数据
const { data: aboutData } = await useFetch('/api/page-data/about')
</script>

7. 代码示例与业务逻辑

API请求封装(composables/useApi.ts)

import type { NitroFetchRequest } from 'nitropack'
import StorageUtil from '~/utils/storage'

export const useApi = <T>(url: NitroFetchRequest, options?: any) => {
  const token = StorageUtil.get<string>('token')
  console.log('useApi   toekn', token) // 确认能获取到token
  return $fetch<T>(url, {
    ...options,
    headers: {
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options?.headers,
    },
    onResponseError: (error) => {
      const status = error.response?.status
      const message = error.response?._data?.message || 'Request failed'
      // 根据状态码处理不同逻辑
      switch (status) {
        case 400:
          console.error('Bad Request:', message)
          break
        case 401:
          console.error('Unauthorized:', message)
          // 清除无效token
          StorageUtil.remove('token')
          navigateTo('/login')
          break
        case 403:
          console.error('Forbidden:', message)
          break
        case 404:
          console.error('Not Found:', message)
          break
        case 500:
          console.error('Server Error:', message)
          break
        default:
          console.error('Unknown Error:', message)
      }

      throw error
    }
  })
}

页面数据获取(composables/useAboutApi.ts)

import { useApi } from '@/composables/useApi';
import { useRouter } from 'vue-router';
import type { HomeData, hata } from '~/types/home';


export const useAboutApi = () => {
  const router = useRouter()

  // 统一错误处理 (useAboutApi)
  const handleError = (error: any) => {
    const message = error.data?.message || error?.message || 'Request failed'
    const code = error.statusCode || 500

    // 显示错误提示(可根据UI库调整)
    console.error(`[PageApi Error] ${message}`)

    // 401 未授权跳转到登录页
    if (code === 401) {
      router.push('/login')
    }

    // 抛出格式化后的错误
    throw {
      code,
      message,
      data: error.data?.data || null
    }
  }

  // 获取首页聚合数据
  const getAboutData = async (): Promise<hata> => {
    try {
      const response = await useApi<HomeData>('/api/about-us', {
        headers: {
          'Content-Type': 'application/json'
        }
      })
      console.log('Home data fetched successfully:', response)
      // 检查响应数据是否符合预期
      // 转换数据格式以适应前端组件
      let data = response.data
      return data
    } catch (error) {
      return handleError(error)
    }
  }



  return {
    getAboutData,
  }
}

关于我们页面实现(pages/about.vue)

<template>
  <div class="max-w-5xl mx-auto px-4 py-12">
    <h1 class="text-4xl font-bold mb-8">{{ pageData.title }}</h1>
    
    <div class="prose max-w-none" v-html="pageData.content"></div>
    
    <section class="mt-16">
      <h2 class="text-2xl font-semibold mb-6">我们的团队</h2>
      <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
        <TeamMember 
          v-for="member in team" 
          :key="member.id"
          :member="member"
        />
      </div>
    </section>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { usePageApi } from '@/composables/usePageApi'

const { getPageData } = usePageApi()
const pageData = ref({ title: '', content: '' })
const team = ref([])

onMounted(async () => {
  const [pageResponse, teamResponse] = await Promise.all([
    getPageData('about'),
    $fetch('/api/team')
  ])
  
  pageData.value = pageResponse.data
  team.value = teamResponse.data
})
</script>

8. 总结

通过本文的实践,我们成功构建了一个基于Nuxt3和Prisma的全栈企业官网解决方案,具备以下特点:

  1. 高性能渲染:利用Nuxt3的SSR能力,提升首屏加载速度和SEO效果
  2. 全栈一体化:在单一项目中实现前后端功能,简化开发和部署
  3. 现代化数据管理:使用Prisma实现类型安全的数据库操作
  4. 响应式设计:通过TailwindCSS快速构建适配各种设备的界面
  5. 模块化架构:清晰的目录结构和组件化设计提高可维护性
  6. 完善的安全机制:基于JWT的认证系统保护API安全

此项目架构非常适合中小企业快速搭建专业官网,既满足品牌展示需求,又为后续功能扩展提供了坚实的基础。开发过程中,Nuxt3的约定式路由和自动导入功能显著提升了开发效率,而Prisma的类型安全特性大大减少了数据库操作中的错误。

对于后续优化,可以考虑:

  1. 实现静态生成(SSG)进一步提高性能
  2. 添加国际化支持
  3. 集成CMS后台管理功能
  4. 增加性能监控和分析工具

项目源码github.com/nuxt3-enterprise-website(持续更新)
mysqlmysql-backups

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐