Nuxt.js基础与进阶
Nuxt.js 简介
Nuxt.js 是一个基于 Vue.js 的开源框架,专门用于构建服务端渲染(SSR)、静态站点生成(SSG)和单页应用(SPA)。它提供了一套约定大于配置的开发体系,大大简化了 Vue 应用的构建和部署过程。
核心特性
- 自动路由生成:基于文件系统的路由
- 服务端渲染:内置 SSR 支持,提升 SEO 和首屏性能
- 静态站点生成:支持预渲染静态页面
- 代码分割:自动代码分割和懒加载
- 插件生态:丰富的模块和插件系统
- 开发体验:热重载、错误页面、调试工具
Nuxt.js 架构原理
graph TD
A[用户请求] --> B{渲染模式判断}
B -->|SSR| C[服务端渲染]
B -->|SPA| D[客户端渲染]
B -->|SSG| E[静态页面]
C --> F[Vue组件执行]
F --> G[数据预取]
G --> H[HTML生成]
H --> I[返回完整页面]
D --> J[加载Vue应用]
J --> K[客户端路由]
K --> L[动态渲染]
E --> M[预构建HTML]
M --> N[CDN分发]
Nuxt.js 核心概念
1. 项目结构
javascript
// 标准 Nuxt.js 项目结构
my-nuxt-app/
├── assets/ // 未编译资源文件
├── components/ // Vue 组件
├── layouts/ // 布局组件
├── middleware/ // 中间件
├── pages/ // 页面组件(自动生成路由)
├── plugins/ // Vue 插件
├── static/ // 静态文件
├── store/ // Vuex 状态管理
├── nuxt.config.js // Nuxt 配置文件
└── package.json
2. 页面组件
javascript
// pages/index.vue - 首页组件
<template>
<div class="container">
<h1>{{ title }}</h1>
<p>欢迎访问 Nuxt.js 应用</p>
</div>
</template>
<script>
export default {
name: 'HomePage',
// 页面头部信息
head() {
return {
title: 'Nuxt.js 首页',
meta: [
{ hid: 'description', name: 'description', content: '这是首页描述' }
]
}
},
// 服务端数据预取
async asyncData({ $axios }) {
const { data } = await $axios.get('/api/content')
return {
title: data.title
}
}
}
</script>
3. 布局系统
javascript
// layouts/default.vue - 默认布局
<template>
<div class="app">
<header class="header">
<nav>
<NuxtLink to="/">首页</NuxtLink>
<NuxtLink to="/about">关于</NuxtLink>
</nav>
</header>
<main class="main">
<!-- 页面内容插槽 -->
<Nuxt />
</main>
<footer class="footer">
<p>© 2024 Nuxt.js 应用</p>
</footer>
</div>
</template>
<script>
export default {
name: 'DefaultLayout'
}
</script>
4. 中间件系统
javascript
// middleware/auth.js - 身份验证中间件
export default function ({ store, redirect }) {
// 检查用户是否已登录
if (!store.state.auth.user) {
return redirect('/login')
}
}
// pages/dashboard.vue - 使用中间件的页面
<template>
<div>
<h1>用户仪表板</h1>
</div>
</template>
<script>
export default {
middleware: 'auth' // 应用认证中间件
}
</script>
渲染模式详解
1. 服务端渲染(SSR)
javascript
// nuxt.config.js - SSR 模式配置
export default {
// SSR 模式(默认)
ssr: true,
// 服务端渲染配置
render: {
// 资源提示
resourceHints: true,
// 内联关键CSS
inlineStyles: true
}
}
// 页面组件中的 SSR 数据预取
export default {
async asyncData({ params, $axios }) {
// 服务端执行,预取数据
const post = await $axios.$get(`/api/posts/${params.id}`)
return { post }
},
async fetch({ store, params }) {
// 服务端和客户端都会执行
await store.dispatch('posts/fetchPost', params.id)
}
}
2. 静态站点生成(SSG)
javascript
// nuxt.config.js - SSG 配置
export default {
// 静态生成模式
target: 'static',
// 生成配置
generate: {
// 动态路由生成
routes() {
return axios.get('/api/posts')
.then(res => res.data.map(post => `/posts/${post.id}`))
},
// 生成间隔
interval: 100,
// 并发数
concurrency: 10
}
}
3. 单页应用(SPA)
javascript
// nuxt.config.js - SPA 模式配置
export default {
// 禁用 SSR,启用 SPA 模式
ssr: false,
// SPA 配置
spa: {
// 加载指示器
loading: '~/components/LoadingIndicator.vue'
}
}
动态路由和嵌套路由
路由生成机制
Nuxt.js 基于文件系统自动生成路由,无需手动配置路由表。
graph TD
A[pages目录结构] --> B[自动路由生成]
B --> C[静态路由]
B --> D[动态路由]
B --> E[嵌套路由]
C --> F["about.vue → /about"]
D --> G["_id.vue → /:id"]
E --> H["user/_id/profile.vue → /user/:id/profile"]
1. 基础路由
javascript
// 文件结构和对应路由
pages/
├── index.vue // → /
├── about.vue // → /about
├── contact.vue // → /contact
└── blog/
├── index.vue // → /blog
└── archive.vue // → /blog/archive
2. 动态路由
单参数动态路由
javascript
// pages/user/_id.vue - 用户详情页
<template>
<div class="user-profile">
<h1>用户: {{ user.name }}</h1>
<p>ID: {{ $route.params.id }}</p>
</div>
</template>
<script>
export default {
async asyncData({ params, $axios }) {
// 根据路由参数获取用户数据
const user = await $axios.$get(`/api/users/${params.id}`)
return { user }
},
// 参数校验
validate({ params }) {
return /^\d+$/.test(params.id)
}
}
</script>
多参数动态路由
javascript
// pages/category/_type/_id.vue - 分类文章页
<template>
<div>
<h1>{{ article.title }}</h1>
<p>分类: {{ $route.params.type }}</p>
<div v-html="article.content"></div>
</div>
</template>
<script>
export default {
async asyncData({ params, $axios }) {
const { type, id } = params
const article = await $axios.$get(`/api/articles/${type}/${id}`)
return { article }
},
// 路径参数: /category/tech/123
// params: { type: 'tech', id: '123' }
}
</script>
可选动态路由
javascript
// pages/shop/_category/_product.vue - 可选分类的商品页
<template>
<div>
<h1>{{ product.name }}</h1>
<p v-if="category">分类: {{ category }}</p>
</div>
</template>
<script>
export default {
async asyncData({ params }) {
const { category, product } = params
// 处理可选参数
const productData = await fetchProduct(product, category)
return {
product: productData,
category
}
}
}
</script>
// 支持路由:
// /shop/electronics/laptop → { category: 'electronics', product: 'laptop' }
// /shop/laptop → { product: 'laptop' }
3. 嵌套路由
基础嵌套路由
javascript
// 文件结构
pages/
├── user/
│ ├── index.vue // → /user
│ ├── _id/
│ │ ├── index.vue // → /user/:id
│ │ ├── profile.vue // → /user/:id/profile
│ │ └── settings.vue // → /user/:id/settings
│ └── create.vue // → /user/create
// pages/user.vue - 父级路由组件
<template>
<div class="user-layout">
<nav class="user-nav">
<NuxtLink :to="`/user/${userId}`">概览</NuxtLink>
<NuxtLink :to="`/user/${userId}/profile`">个人信息</NuxtLink>
<NuxtLink :to="`/user/${userId}/settings`">设置</NuxtLink>
</nav>
<!-- 子路由内容 -->
<NuxtChild :user="user" />
</div>
</template>
<script>
export default {
async asyncData({ params }) {
const user = await fetchUser(params.id)
return { user, userId: params.id }
}
}
</script>
复杂嵌套路由
javascript
// pages/admin/_module/_action.vue - 管理后台嵌套路由
<template>
<div class="admin-panel">
<AdminSidebar :module="$route.params.module" />
<div class="admin-content">
<component
:is="actionComponent"
:module="$route.params.module"
:action="$route.params.action"
/>
</div>
</div>
</template>
<script>
export default {
computed: {
actionComponent() {
const { module, action } = this.$route.params
return () => import(`~/components/admin/${module}/${action}.vue`)
}
},
middleware: 'admin',
// 支持路由如: /admin/users/list, /admin/posts/edit
}
</script>
4. 路由参数和查询
路由参数处理
javascript
// pages/search/_query.vue - 搜索结果页
<template>
<div class="search-results">
<h1>搜索: "{{ $route.params.query }}"</h1>
<div class="filters">
<select v-model="category" @change="updateQuery">
<option value="">所有分类</option>
<option value="tech">技术</option>
<option value="design">设计</option>
</select>
</div>
<div class="results">
<div v-for="item in results" :key="item.id">
{{ item.title }}
</div>
</div>
</div>
</template>
<script>
export default {
async asyncData({ params, query }) {
const searchQuery = params.query
const category = query.category || ''
const results = await searchContent(searchQuery, {
category,
page: query.page || 1,
limit: query.limit || 10
})
return { results, category }
},
methods: {
updateQuery() {
this.$router.push({
path: this.$route.path,
query: {
...this.$route.query,
category: this.category,
page: 1 // 重置页码
}
})
}
},
watchQuery: ['category', 'page'] // 监听查询参数变化
}
</script>
5. 路由守卫和中间件
页面级中间件
javascript
// middleware/auth.js - 认证中间件
export default function ({ store, redirect, route }) {
// 检查登录状态
if (!store.state.auth.user) {
// 保存当前路径,登录后跳转回来
return redirect('/login?redirect=' + route.fullPath)
}
// 检查权限
const requiredRole = route.meta?.role
if (requiredRole && !store.state.auth.user.roles.includes(requiredRole)) {
throw new Error('权限不足')
}
}
// pages/admin/_id.vue - 使用中间件
export default {
middleware: ['auth'],
meta: {
role: 'admin' // 需要管理员权限
}
}
全局中间件
javascript
// middleware/analytics.js - 统计中间件
export default function ({ route }) {
// 页面访问统计
if (process.client) {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: route.fullPath
})
}
}
// nuxt.config.js - 注册全局中间件
export default {
router: {
middleware: 'analytics'
}
}
6. 程序化导航
javascript
// 在组件中使用程序化导航
export default {
methods: {
// 基础导航
goToUser(userId) {
this.$router.push(`/user/${userId}`)
},
// 带查询参数导航
searchWithFilters(query, filters) {
this.$router.push({
path: `/search/${query}`,
query: filters
})
},
// 替换当前路由
replaceRoute() {
this.$router.replace('/new-path')
},
// 历史导航
goBack() {
this.$router.go(-1)
}
}
}
7. 路由生成和预取
javascript
// nuxt.config.js - 动态路由生成配置
export default {
generate: {
routes() {
// 生成用户页面路由
return axios.get('/api/users')
.then(res => {
return res.data.map(user => ({
route: `/user/${user.id}`,
payload: user // 预设数据
}))
})
}
}
}
// 使用预设数据
export default {
async asyncData({ params, payload }) {
// 优先使用预设数据,减少API调用
if (payload) {
return { user: payload }
}
// 回退到API获取
const user = await $axios.$get(`/api/users/${params.id}`)
return { user }
}
}
SEO优化
SEO优化流程
graph TD
A[页面请求] --> B[Meta标签生成]
B --> C[结构化数据]
C --> D[Open Graph标签]
D --> E[Sitemap生成]
E --> F[搜索引擎抓取]
F --> G[页面索引]
G --> H[搜索结果展示]
1. 页面Meta信息
动态Meta标签
javascript
// pages/blog/_slug.vue - 博客文章页面
<template>
<article>
<h1>{{ post.title }}</h1>
<div v-html="post.content"></div>
</article>
</template>
<script>
export default {
async asyncData({ params, $axios }) {
const post = await $axios.$get(`/api/posts/${params.slug}`)
return { post }
},
head() {
const post = this.post
return {
title: post.title,
meta: [
{
hid: 'description',
name: 'description',
content: post.summary
},
{
hid: 'keywords',
name: 'keywords',
content: post.tags.join(', ')
},
// Open Graph 标签
{
hid: 'og:title',
property: 'og:title',
content: post.title
},
{
hid: 'og:description',
property: 'og:description',
content: post.summary
},
{
hid: 'og:image',
property: 'og:image',
content: post.image
},
{
hid: 'og:url',
property: 'og:url',
content: `https://example.com/blog/${post.slug}`
},
// Twitter Card 标签
{
hid: 'twitter:card',
name: 'twitter:card',
content: 'summary_large_image'
},
{
hid: 'twitter:title',
name: 'twitter:title',
content: post.title
}
],
link: [
{
rel: 'canonical',
href: `https://example.com/blog/${post.slug}`
}
]
}
}
}
</script>
全局Meta配置
javascript
// nuxt.config.js - 全局SEO配置
export default {
head: {
titleTemplate: '%s | 我的网站',
htmlAttrs: {
lang: 'zh-CN'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: '这是我的网站描述'
},
// 默认 Open Graph 标签
{ property: 'og:type', content: 'website' },
{ property: 'og:locale', content: 'zh_CN' },
{ property: 'og:site_name', content: '我的网站' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
}
2. 结构化数据
JSON-LD 结构化数据
javascript
// components/StructuredData.vue - 结构化数据组件
<template>
<div>
<!-- JSON-LD 结构化数据 -->
<script type="application/ld+json" v-html="jsonLD"></script>
</div>
</template>
<script>
export default {
props: {
type: String,
data: Object
},
computed: {
jsonLD() {
const schemas = {
article: this.articleSchema,
product: this.productSchema,
organization: this.organizationSchema
}
return JSON.stringify(schemas[this.type] || {})
},
articleSchema() {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: this.data.title,
description: this.data.summary,
image: this.data.image,
author: {
'@type': 'Person',
name: this.data.author.name
},
publisher: {
'@type': 'Organization',
name: '我的网站',
logo: {
'@type': 'ImageObject',
url: 'https://example.com/logo.png'
}
},
datePublished: this.data.publishedAt,
dateModified: this.data.updatedAt
}
},
productSchema() {
return {
'@context': 'https://schema.org',
'@type': 'Product',
name: this.data.name,
description: this.data.description,
image: this.data.images,
offers: {
'@type': 'Offer',
price: this.data.price,
priceCurrency: 'CNY',
availability: 'https://schema.org/InStock'
}
}
}
}
}
</script>
使用结构化数据
javascript
// pages/blog/_slug.vue - 在页面中使用结构化数据
<template>
<div>
<StructuredData type="article" :data="post" />
<article>
<h1>{{ post.title }}</h1>
<div v-html="post.content"></div>
</article>
</div>
</template>
3. 网站地图生成
自动生成Sitemap
javascript
// nuxt.config.js - Sitemap 配置
export default {
modules: [
'@nuxtjs/sitemap'
],
sitemap: {
hostname: 'https://example.com',
gzip: true,
exclude: [
'/admin/**',
'/private'
],
routes: async () => {
// 动态生成路由
const { data } = await axios.get('/api/posts')
return data.map(post => `/blog/${post.slug}`)
},
defaults: {
changefreq: 'daily',
priority: 1,
lastmod: new Date()
}
}
}
自定义Sitemap
javascript
// 手动生成 sitemap.xml
export default {
generate: {
async done(generator) {
// 生成完成后创建sitemap
const sitemap = await generateSitemap()
await fs.writeFile('dist/sitemap.xml', sitemap)
}
}
}
async function generateSitemap() {
const urls = [
{ loc: '/', priority: 1.0 },
{ loc: '/about', priority: 0.8 }
]
// 添加动态页面
const posts = await fetchPosts()
posts.forEach(post => {
urls.push({
loc: `/blog/${post.slug}`,
lastmod: post.updatedAt,
priority: 0.6
})
})
return generateXML(urls)
}
4. 性能优化与SEO
关键资源优化
javascript
// nuxt.config.js - 性能优化配置
export default {
render: {
// 资源提示
resourceHints: true,
// 内联关键CSS
inlineStyles: true,
// 预加载策略
bundleRenderer: {
shouldPreload: (file, type) => {
return ['script', 'style', 'font'].includes(type)
}
}
},
// 图片优化
image: {
// 响应式图片
responsive: {
placeholder: true,
quality: 80
}
}
}
懒加载和预取
javascript
// components/LazyImage.vue - 懒加载图片组件
<template>
<div class="lazy-image">
<img
v-if="loaded"
:src="src"
:alt="alt"
@load="onLoad"
>
<div v-else class="placeholder">
Loading...
</div>
</div>
</template>
<script>
export default {
props: ['src', 'alt'],
data() {
return { loaded: false }
},
mounted() {
// 使用 Intersection Observer 实现懒加载
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.loaded = true
observer.disconnect()
}
})
observer.observe(this.$el)
}
}
</script>
5. 多语言SEO
国际化配置
javascript
// nuxt.config.js - 多语言配置
export default {
modules: [
'@nuxtjs/i18n'
],
i18n: {
locales: [
{ code: 'zh', iso: 'zh-CN', file: 'zh.js' },
{ code: 'en', iso: 'en-US', file: 'en.js' }
],
defaultLocale: 'zh',
strategy: 'prefix_except_default',
seo: true, // 自动生成hreflang标签
baseUrl: 'https://example.com'
}
}
多语言页面SEO
javascript
// pages/blog/_slug.vue - 多语言SEO
export default {
head() {
const post = this.post
const locale = this.$i18n.locale
return {
title: post.title[locale],
meta: [
{
hid: 'description',
name: 'description',
content: post.summary[locale]
},
{
hid: 'og:locale',
property: 'og:locale',
content: locale === 'zh' ? 'zh_CN' : 'en_US'
}
],
link: [
// 规范链接
{
rel: 'canonical',
href: `https://example.com${this.localePath(this.$route)}`
},
// 多语言链接
{
rel: 'alternate',
hreflang: 'zh-CN',
href: `https://example.com${this.switchLocalePath('zh')}`
},
{
rel: 'alternate',
hreflang: 'en-US',
href: `https://example.com${this.switchLocalePath('en')}`
}
]
}
}
}
6. SEO监控和分析
Google Analytics 集成
javascript
// plugins/gtag.client.js - GA4 集成
export default ({ $gtag, app }) => {
// 页面浏览追踪
app.router.afterEach((to) => {
$gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.fullPath,
page_title: document.title
})
})
}
// nuxt.config.js
export default {
plugins: [
{ src: '~/plugins/gtag.client.js', mode: 'client' }
],
head: {
script: [
{
async: true,
src: 'https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID'
}
]
}
}
SEO检查工具
javascript
// utils/seo-checker.js - SEO检查工具
export function checkSEO(page) {
const issues = []
// 检查标题长度
if (!page.title || page.title.length < 30 || page.title.length > 60) {
issues.push('标题长度不合适(建议30-60字符)')
}
// 检查描述
if (!page.description || page.description.length < 120 || page.description.length > 160) {
issues.push('描述长度不合适(建议120-160字符)')
}
// 检查关键词
if (!page.keywords || page.keywords.length === 0) {
issues.push('缺少关键词')
}
// 检查图片alt属性
const images = page.querySelectorAll('img')
images.forEach(img => {
if (!img.alt) {
issues.push(`图片缺少alt属性: ${img.src}`)
}
})
return issues
}
7. Schema.org 标记
完整的页面Schema
javascript
// mixins/schema.js - Schema标记混入
export default {
head() {
return {
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify(this.pageSchema)
}
]
}
},
computed: {
pageSchema() {
return {
'@context': 'https://schema.org',
'@graph': [
this.websiteSchema,
this.organizationSchema,
this.breadcrumbSchema,
this.pageSpecificSchema
]
}
},
websiteSchema() {
return {
'@type': 'WebSite',
'@id': 'https://example.com/#website',
url: 'https://example.com',
name: '我的网站',
potentialAction: {
'@type': 'SearchAction',
target: 'https://example.com/search?q={search_term_string}',
'query-input': 'required name=search_term_string'
}
}
},
breadcrumbSchema() {
const breadcrumbs = this.generateBreadcrumbs()
return {
'@type': 'BreadcrumbList',
itemListElement: breadcrumbs.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url
}))
}
}
}
}
部署 Nuxt.js 应用
部署流程
graph TD
A[开发完成] --> B{部署模式选择}
B -->|SSR| C[服务端渲染部署]
B -->|SSG| D[静态站点生成]
B -->|SPA| E[单页应用部署]
C --> F[Node.js服务器]
F --> G[PM2进程管理]
G --> H[Nginx反向代理]
D --> I[静态文件生成]
I --> J[CDN分发]
E --> K[构建SPA]
K --> L[静态服务器]
1. 静态站点生成(SSG)部署
生成静态站点
javascript
// nuxt.config.js - SSG 配置
export default {
target: 'static',
generate: {
// 并发生成数
concurrency: 25,
// 生成间隔
interval: 100,
// 动态路由生成
routes: async () => {
const routes = []
// 生成博客文章路由
const posts = await fetchPosts()
posts.forEach(post => {
routes.push(`/blog/${post.slug}`)
})
// 生成用户页面路由
const users = await fetchUsers()
users.forEach(user => {
routes.push(`/user/${user.id}`)
})
return routes
},
// 排除路径
exclude: [
/^\/admin/, // 排除管理页面
/^\/api/ // 排除API路由
]
}
}
Netlify 部署
javascript
// netlify.toml - Netlify 配置
[build]
command = "npm run generate"
publish = "dist"
[build.environment]
NODE_VERSION = "16"
[[redirects]]
from = "/api/*"
to = "https://api.example.com/:splat"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
# package.json 脚本
{
"scripts": {
"build": "nuxt build",
"generate": "nuxt generate",
"deploy": "npm run generate && netlify deploy --prod --dir=dist"
}
}
GitHub Pages 部署
javascript
// .github/workflows/deploy.yml - GitHub Actions
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate static files
run: npm run generate
env:
NUXT_ENV_BASE_URL: https://username.github.io/repository-name
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
# nuxt.config.js - GitHub Pages 配置
export default {
target: 'static',
router: {
base: process.env.NODE_ENV === 'production' ? '/repository-name/' : '/'
}
}
2. 服务端渲染(SSR)部署
PM2 进程管理
javascript
// ecosystem.config.js - PM2 配置
module.exports = {
apps: [
{
name: 'nuxt-app',
exec_mode: 'cluster',
instances: 'max',
script: './node_modules/nuxt/bin/nuxt.js',
args: 'start',
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true
}
]
}
// 部署脚本
{
"scripts": {
"build": "nuxt build",
"start": "nuxt start",
"pm2:start": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop ecosystem.config.js",
"pm2:reload": "pm2 reload ecosystem.config.js"
}
}
Docker 部署
dockerfile
# Dockerfile
FROM node:16-alpine
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production && npm cache clean --force
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 暴露端口
EXPOSE 3000
# 启动应用
CMD ["npm", "start"]
yaml
# docker-compose.yml
version: '3.8'
services:
nuxt-app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/ssl
depends_on:
- nuxt-app
restart: unless-stopped
Nginx 配置
nginx
# nginx.conf
upstream nuxt {
server nuxt-app:3000;
}
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
# 静态资源缓存
location /_nuxt/ {
alias /app/.nuxt/dist/client/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 代理到 Nuxt.js 应用
location / {
proxy_pass http://nuxt;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
3. Vercel 部署
javascript
// vercel.json - Vercel 配置
{
"version": 2,
"builds": [
{
"src": "nuxt.config.js",
"use": "@nuxtjs/vercel-builder",
"config": {
"serverFiles": ["package.json"]
}
}
],
"routes": [
{
"src": "/sw.js",
"continue": true,
"headers": {
"Cache-Control": "public, max-age=0, must-revalidate",
"Service-Worker-Allowed": "/"
}
}
]
}
// nuxt.config.js - Vercel 优化
export default {
// Vercel 环境变量
publicRuntimeConfig: {
baseURL: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000'
},
// 构建优化
build: {
// 分析包大小
analyze: process.env.ANALYZE === 'true',
// 提取CSS
extractCSS: true,
// 优化
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
}
4. 服务器优化配置
环境变量管理
javascript
// .env.production
NODE_ENV=production
BASE_URL=https://example.com
API_URL=https://api.example.com
NUXT_SECRET_KEY=your-secret-key
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=production_db
DB_USER=db_user
DB_PASS=secure_password
// nuxt.config.js - 环境变量配置
export default {
// 公开运行时配置
publicRuntimeConfig: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
apiURL: process.env.API_URL || 'http://localhost:8000'
},
// 私有运行时配置
privateRuntimeConfig: {
secretKey: process.env.NUXT_SECRET_KEY,
dbConfig: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASS
}
}
}
健康检查和监控
javascript
// server/api/health.js - 健康检查端点
export default function (req, res) {
const health = {
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.env.npm_package_version
}
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(health))
}
// plugins/monitoring.js - 错误监控
export default ({ $axios, $sentry }) => {
// API 错误监控
$axios.onError(error => {
if (process.client) {
$sentry.captureException(error)
}
console.error('API Error:', error)
})
// 全局错误处理
if (process.client) {
window.addEventListener('error', event => {
$sentry.captureException(event.error)
})
window.addEventListener('unhandledrejection', event => {
$sentry.captureException(event.reason)
})
}
}
5. 部署自动化
CI/CD 流水线
yaml
# .github/workflows/deploy.yml
name: Deploy Production
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
NODE_ENV: production
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/nuxt-app
git pull origin main
npm ci --production
npm run build
pm2 reload ecosystem.config.js
蓝绿部署
bash
#!/bin/bash
# deploy.sh - 蓝绿部署脚本
APP_NAME="nuxt-app"
CURRENT_ENV=$(pm2 list | grep $APP_NAME | grep online | head -1 | awk '{print $4}')
if [ "$CURRENT_ENV" = "blue" ]; then
NEW_ENV="green"
NEW_PORT=3001
else
NEW_ENV="blue"
NEW_PORT=3000
fi
echo "部署到 $NEW_ENV 环境 (端口: $NEW_PORT)"
# 构建新版本
npm run build
# 启动新环境
PORT=$NEW_PORT NODE_ENV=production pm2 start ecosystem.config.js --name "$APP_NAME-$NEW_ENV"
# 健康检查
sleep 10
if curl -f http://localhost:$NEW_PORT/api/health; then
echo "新环境健康检查通过"
# 更新 Nginx 配置
sed -i "s/server nuxt-app:.*/server nuxt-app:$NEW_PORT;/" /etc/nginx/nginx.conf
nginx -s reload
# 停止旧环境
pm2 delete "$APP_NAME-$CURRENT_ENV" 2>/dev/null || true
echo "部署完成"
else
echo "新环境健康检查失败,回滚"
pm2 delete "$APP_NAME-$NEW_ENV"
exit 1
fi
性能优化
构建优化策略
graph TD
A[性能优化] --> B[构建优化]
A --> C[运行时优化]
A --> D[资源优化]
B --> E[代码分割]
B --> F[Tree Shaking]
B --> G[Bundle分析]
C --> H[缓存策略]
C --> I[懒加载]
C --> J[预取预加载]
D --> K[图片优化]
D --> L[字体优化]
D --> M[CSS优化]
1. 代码分割和懒加载
javascript
// nuxt.config.js - 代码分割配置
export default {
build: {
// 分析bundle大小
analyze: process.env.ANALYZE === 'true',
// 优化配置
optimization: {
splitChunks: {
layouts: true,
pages: true,
commons: true,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
minSize: 30000
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
enforce: true
}
}
}
},
// Babel 配置优化
babel: {
compact: true,
presets({ isServer }) {
return [
[
'@nuxt/babel-preset-app',
{
corejs: { version: 3 },
targets: isServer
? { node: '16' }
: { browsers: ['> 1%', 'last 2 versions'] }
}
]
]
}
}
}
}
2. 资源优化
javascript
// 图片优化配置
export default {
modules: [
'@nuxt/image'
],
image: {
// 图片格式优化
formats: ['webp', 'avif'],
// 响应式图片
screens: {
xs: 320,
sm: 640,
md: 768,
lg: 1024,
xl: 1280
},
// 图片质量
quality: 80,
// 图片提供商
providers: {
cloudinary: {
baseURL: 'https://res.cloudinary.com/your-cloud/image/fetch/'
}
}
},
// CSS 优化
css: [
// 关键CSS内联
{ src: '~/assets/css/critical.css', inline: true },
// 非关键CSS异步加载
'~/assets/css/main.css'
]
}
// components/OptimizedImage.vue - 优化的图片组件
<template>
<NuxtImg
:src="src"
:alt="alt"
:sizes="sizes"
:loading="loading"
:placeholder="placeholder"
format="webp"
quality="80"
@load="onLoad"
/>
</template>
<script>
export default {
props: {
src: String,
alt: String,
sizes: { type: String, default: '100vw' },
loading: { type: String, default: 'lazy' },
placeholder: { type: Boolean, default: true }
},
methods: {
onLoad() {
this.$emit('load')
}
}
}
</script>
3. 缓存策略
javascript
// nuxt.config.js - 缓存配置
export default {
// 页面级缓存
render: {
// 静态资源缓存
static: {
maxAge: 1000 * 60 * 60 * 24 * 7 // 7天
},
// 压缩配置
compressor: {
threshold: 0
}
},
// HTTP/2 Push
http2: {
push: true,
pushAssets: (req, res, publicPath, preloadFiles) => {
// 推送关键资源
return preloadFiles
.filter(f => f.asType === 'script' && f.file === 'runtime.js')
.map(f => `<${publicPath}${f.file}>; rel=preload; as=${f.asType}`)
}
}
}
// 服务端缓存中间件
// middleware/cache.js
import LRU from 'lru-cache'
const cache = new LRU({
max: 1000,
ttl: 1000 * 60 * 15 // 15分钟
})
export default function (req, res, next) {
const key = req.url
// 检查缓存
if (cache.has(key)) {
const cached = cache.get(key)
res.setHeader('X-Cache', 'HIT')
return res.end(cached)
}
// 拦截响应
const originalEnd = res.end
res.end = function(chunk, encoding) {
if (res.statusCode === 200) {
cache.set(key, chunk)
res.setHeader('X-Cache', 'MISS')
}
originalEnd.call(res, chunk, encoding)
}
next()
}
4. 预取和预加载
javascript
// plugins/preload.client.js - 智能预加载
export default function ({ app, router }) {
// 路由预取
router.beforeEach((to, from, next) => {
// 预取下一个可能的路由
const predictedRoutes = predictNextRoutes(to.path)
predictedRoutes.forEach(route => {
router.resolve(route).route.matched.forEach(m => {
if (m.components.default && typeof m.components.default === 'function') {
m.components.default()
}
})
})
next()
})
// 图片预加载
const preloadImages = () => {
const images = document.querySelectorAll('img[data-preload]')
images.forEach(img => {
const link = document.createElement('link')
link.rel = 'preload'
link.as = 'image'
link.href = img.dataset.preload
document.head.appendChild(link)
})
}
// 页面加载完成后预加载
window.addEventListener('load', preloadImages)
}
function predictNextRoutes(currentPath) {
// 基于用户行为预测下一个路由
const predictions = {
'/': ['/blog', '/about'],
'/blog': ['/blog/[slug]'],
'/user/[id]': ['/user/[id]/profile', '/user/[id]/settings']
}
return predictions[currentPath] || []
}
5. 运行时性能监控
javascript
// plugins/performance.client.js - 性能监控
export default function ({ $gtag }) {
// Web Vitals 监控
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)
})
function sendToAnalytics({ name, value, id }) {
$gtag('event', name, {
event_category: 'Web Vitals',
event_label: id,
value: Math.round(name === 'CLS' ? value * 1000 : value),
non_interaction: true
})
}
// 页面加载时间监控
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0]
const loadTime = navigation.loadEventEnd - navigation.fetchStart
$gtag('event', 'page_load_time', {
event_category: 'Performance',
value: Math.round(loadTime),
non_interaction: true
})
})
}
6. PWA 优化
javascript
// nuxt.config.js - PWA 配置
export default {
modules: [
'@nuxtjs/pwa'
],
pwa: {
// 离线缓存策略
workbox: {
runtimeCaching: [
{
urlPattern: 'https://api\\.example\\.com/.*',
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
cacheableResponse: {
statuses: [0, 200]
},
networkTimeoutSeconds: 3
}
},
{
urlPattern: '.*\\.(png|jpg|jpeg|svg|gif)$',
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30天
}
}
}
]
},
// 清单文件
manifest: {
name: 'Nuxt.js 应用',
short_name: 'NuxtApp',
description: 'Nuxt.js PWA 应用',
theme_color: '#2196F3',
background_color: '#ffffff'
}
}
}
7. 内存和性能优化
javascript
// utils/performance.js - 性能优化工具
export class PerformanceOptimizer {
constructor() {
this.observers = new Map()
this.rafId = null
}
// 防抖函数
debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
// 节流函数
throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
// Intersection Observer 懒加载
lazyLoad(elements, callback) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback(entry.target)
observer.unobserve(entry.target)
}
})
}, {
rootMargin: '50px 0px',
threshold: 0.1
})
elements.forEach(el => observer.observe(el))
this.observers.set('lazy', observer)
}
// RequestAnimationFrame 优化
scheduleUpdate(callback) {
if (this.rafId) {
cancelAnimationFrame(this.rafId)
}
this.rafId = requestAnimationFrame(() => {
callback()
this.rafId = null
})
}
// 清理资源
cleanup() {
this.observers.forEach(observer => observer.disconnect())
this.observers.clear()
if (this.rafId) {
cancelAnimationFrame(this.rafId)
}
}
}
// 使用示例
export default {
mounted() {
this.optimizer = new PerformanceOptimizer()
// 懒加载图片
const images = this.$el.querySelectorAll('img[data-lazy]')
this.optimizer.lazyLoad(images, (img) => {
img.src = img.dataset.lazy
img.removeAttribute('data-lazy')
})
},
beforeDestroy() {
if (this.optimizer) {
this.optimizer.cleanup()
}
}
}
模块系统和扩展
Nuxt 模块生态
graph TD
A[Nuxt 核心] --> B[官方模块]
A --> C[社区模块]
A --> D[自定义模块]
B --> E["@nuxtjs/axios"]
B --> F["@nuxtjs/pwa"]
B --> G["@nuxtjs/content"]
C --> H["@nuxtjs/tailwindcss"]
C --> I["@nuxtjs/storybook"]
C --> J["@nuxtjs/google-analytics"]
D --> K[业务模块]
D --> L[工具模块]
1. 常用官方模块
@nuxtjs/axios - HTTP 客户端
javascript
// nuxt.config.js
export default {
modules: [
'@nuxtjs/axios'
],
axios: {
baseURL: 'https://api.example.com',
retry: { retries: 3 },
// 请求拦截器
interceptors: {
request: [
(config) => {
config.headers.common['Authorization'] = `Bearer ${getToken()}`
return config
}
],
response: [
(response) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
// 重定向到登录页
return redirect('/login')
}
return Promise.reject(error)
}
]
}
}
}
// 在组件中使用
export default {
async asyncData({ $axios }) {
const posts = await $axios.$get('/posts')
return { posts }
},
methods: {
async createPost() {
try {
const post = await this.$axios.$post('/posts', this.postData)
this.$router.push(`/posts/${post.id}`)
} catch (error) {
this.$toast.error('创建失败')
}
}
}
}
@nuxtjs/content - 内容管理
javascript
// nuxt.config.js
export default {
modules: [
'@nuxt/content'
],
content: {
// Markdown 配置
markdown: {
prism: {
theme: 'prism-themes/themes/prism-material-oceanic.css'
}
},
// 实时编辑
liveEdit: false
}
}
// pages/blog/_slug.vue - 内容页面
<template>
<div>
<h1>{{ article.title }}</h1>
<p>{{ article.description }}</p>
<nuxt-content :document="article" />
</div>
</template>
<script>
export default {
async asyncData({ $content, params }) {
const article = await $content('articles', params.slug).fetch()
return {
article
}
},
head() {
return {
title: this.article.title,
meta: [
{
hid: 'description',
name: 'description',
content: this.article.description
}
]
}
}
}
</script>
// content/articles/hello-world.md
---
title: Hello World
description: 我的第一篇文章
img: /images/hello-world.jpg
alt: Hello World
author:
name: 张三
bio: 前端开发工程师
tags:
- nuxtjs
- markdown
---
## 介绍
这是我的第一篇文章,使用 Nuxt Content 编写。
```javascript
console.log('Hello World')
css
#### @nuxtjs/pwa - 渐进式 Web 应用
```javascript
// nuxt.config.js
export default {
modules: [
'@nuxtjs/pwa'
],
pwa: {
meta: {
title: '我的PWA应用',
author: '张三'
},
manifest: {
name: '我的PWA应用',
short_name: 'MyPWA',
description: '基于 Nuxt.js 的 PWA 应用',
theme_color: '#2196F3',
background_color: '#ffffff',
display: 'standalone',
orientation: 'portrait',
start_url: '/',
icons: [
{
src: '/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
}
]
},
workbox: {
// 缓存策略
runtimeCaching: [
{
urlPattern: 'https://fonts\\.googleapis\\.com/.*',
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: {
maxEntries: 30,
maxAgeSeconds: 60 * 60 * 24 * 30
}
}
}
]
}
}
}
2. 自定义模块开发
基础模块结构
javascript
// modules/my-module/index.js
const { resolve } = require('path')
module.exports = function MyModule(moduleOptions) {
const options = {
...this.options.myModule,
...moduleOptions
}
// 添加插件
this.addPlugin({
src: resolve(__dirname, 'plugin.js'),
fileName: 'my-module.js',
options
})
// 添加中间件
this.addServerMiddleware({
path: '/api/my-module',
handler: resolve(__dirname, 'middleware.js')
})
// 扩展路由
this.extendRoutes((routes, resolve) => {
routes.push({
name: 'my-module',
path: '/my-module',
component: resolve(__dirname, 'pages/index.vue')
})
})
// 添加构建插件
this.options.build.plugins.push({
apply(compiler) {
// Webpack 插件逻辑
}
})
}
// 模块元信息
module.exports.meta = require('./package.json')
插件文件
javascript
// modules/my-module/plugin.js
import MyModuleService from './service'
export default function ({ $axios, app }, inject) {
// 创建服务实例
const myModuleService = new MyModuleService($axios, <%= JSON.stringify(options) %>)
// 注入到 Vue 实例
inject('myModule', myModuleService)
// 添加到 Nuxt 上下文
app.$myModule = myModuleService
}
// modules/my-module/service.js
export default class MyModuleService {
constructor(axios, options) {
this.$axios = axios
this.options = options
}
async getData() {
try {
const { data } = await this.$axios.get('/api/data')
return data
} catch (error) {
console.error('MyModule Error:', error)
throw error
}
}
processData(data) {
return data.map(item => ({
...item,
processed: true
}))
}
}
3. 插件系统
全局插件
javascript
// plugins/global-components.js
import Vue from 'vue'
import BaseButton from '~/components/BaseButton.vue'
import BaseInput from '~/components/BaseInput.vue'
import BaseModal from '~/components/BaseModal.vue'
// 全局注册组件
Vue.component('BaseButton', BaseButton)
Vue.component('BaseInput', BaseInput)
Vue.component('BaseModal', BaseModal)
// plugins/filters.js
import Vue from 'vue'
// 全局过滤器
Vue.filter('currency', (value) => {
if (!value) return ''
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(value)
})
Vue.filter('dateFormat', (value, format = 'YYYY-MM-DD') => {
if (!value) return ''
return dayjs(value).format(format)
})
客户端专用插件
javascript
// plugins/client-only.client.js
export default ({ app }) => {
// 只在客户端执行
if (process.client) {
// 加载第三方库
import('aos').then(AOS => {
AOS.init({
duration: 1000,
once: true
})
})
// 全局错误处理
window.addEventListener('error', (event) => {
console.error('Client Error:', event.error)
// 发送错误报告
app.$sentry.captureException(event.error)
})
}
}
4. 扩展 Webpack 配置
javascript
// nuxt.config.js
export default {
build: {
extend(config, { isDev, isClient, isServer }) {
// 添加 loader
config.module.rules.push({
test: /\.(glsl|vs|fs)$/,
loader: 'raw-loader'
})
// 添加别名
config.resolve.alias['@assets'] = path.resolve(__dirname, 'assets')
config.resolve.alias['@utils'] = path.resolve(__dirname, 'utils')
// 开发环境配置
if (isDev && isClient) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/
})
}
// 生产环境优化
if (!isDev) {
// 压缩图片
config.module.rules.push({
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: 'imagemin-webpack-loader',
options: {
mozjpeg: { quality: 80 },
pngquant: { quality: [0.65, 0.8] }
}
}
})
}
},
// 添加 Webpack 插件
plugins: [
new webpack.DefinePlugin({
'process.env.BUILD_TIME': JSON.stringify(new Date().toISOString())
})
]
}
}
5. TypeScript 支持
javascript
// nuxt.config.js
export default {
modules: [
'@nuxt/typescript-build'
],
typescript: {
typeCheck: {
eslint: {
files: './**/*.{ts,js,vue}'
}
}
}
}
// types/index.ts - 类型定义
export interface User {
id: number
name: string
email: string
avatar?: string
}
export interface Post {
id: number
title: string
content: string
author: User
createdAt: string
updatedAt: string
}
// pages/blog/_slug.vue - TypeScript 组件
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { Post } from '~/types'
@Component({
async asyncData({ $content, params }) {
const post: Post = await $content('posts', params.slug).fetch()
return { post }
}
})
export default class BlogPost extends Vue {
post!: Post
get formattedDate(): string {
return new Date(this.post.createdAt).toLocaleDateString('zh-CN')
}
head() {
return {
title: this.post.title,
meta: [
{
hid: 'description',
name: 'description',
content: this.post.content.substring(0, 160)
}
]
}
}
}
</script>
6. 测试配置
javascript
// nuxt.config.js
export default {
modules: [
'@nuxtjs/jest'
],
jest: {
collectCoverage: true,
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'<rootDir>/components/**/*.vue',
'<rootDir>/pages/**/*.vue',
'<rootDir>/plugins/**/*.js',
'<rootDir>/store/**/*.js'
]
}
}
// tests/components/BaseButton.spec.js
import { mount } from '@vue/test-utils'
import BaseButton from '@/components/BaseButton.vue'
describe('BaseButton', () => {
test('renders correctly', () => {
const wrapper = mount(BaseButton, {
slots: {
default: 'Click me'
}
})
expect(wrapper.text()).toBe('Click me')
expect(wrapper.classes()).toContain('btn')
})
test('emits click event', async () => {
const wrapper = mount(BaseButton)
await wrapper.trigger('click')
expect(wrapper.emitted().click).toBeTruthy()
})
})