从零搭建SSR企业官网:Nuxt 3 + TypeScript + Vue 3 + TailwindCSS
高性能渲染:利用Nuxt3的SSR能力,提升首屏加载速度和SEO效果全栈一体化:在单一项目中实现前后端功能,简化开发和部署现代化数据管理:使用Prisma实现类型安全的数据库操作响应式设计:通过TailwindCSS快速构建适配各种设备的界面模块化架构:清晰的目录结构和组件化设计提高可维护性完善的安全机制:基于JWT的认证系统保护API安全此项目架构非常适合中小企业快速搭建专业官网,既满足品牌展示
·
从零搭建企业官网: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的全栈企业官网解决方案,具备以下特点:
- 高性能渲染:利用Nuxt3的SSR能力,提升首屏加载速度和SEO效果
- 全栈一体化:在单一项目中实现前后端功能,简化开发和部署
- 现代化数据管理:使用Prisma实现类型安全的数据库操作
- 响应式设计:通过TailwindCSS快速构建适配各种设备的界面
- 模块化架构:清晰的目录结构和组件化设计提高可维护性
- 完善的安全机制:基于JWT的认证系统保护API安全
此项目架构非常适合中小企业快速搭建专业官网,既满足品牌展示需求,又为后续功能扩展提供了坚实的基础。开发过程中,Nuxt3的约定式路由和自动导入功能显著提升了开发效率,而Prisma的类型安全特性大大减少了数据库操作中的错误。
对于后续优化,可以考虑:
- 实现静态生成(SSG)进一步提高性能
- 添加国际化支持
- 集成CMS后台管理功能
- 增加性能监控和分析工具
项目源码:github.com/nuxt3-enterprise-website(持续更新)
mysql:mysql-backups
更多推荐


所有评论(0)