Nuxt.js核心功能项目实战

学习目标

通过本章节的学习,你将能够:

  • 掌握Nuxt.js项目的需求分析方法
  • 了解技术方案设计的流程和要点
  • 熟练应用数据获取和状态管理技术
  • 掌握路由和导航的高级配置
  • 了解错误处理和国际化的实现方式
  • 掌握性能优化和部署的实践技巧

核心知识点

项目需求分析

在开始项目实战之前,我们需要先进行需求分析,明确项目的功能和目标。

项目背景

我们将创建一个电商网站,包含以下功能:

  1. 商品展示:展示商品列表和详情
  2. 用户系统:注册、登录、个人中心
  3. 购物车:添加商品、修改数量、删除商品
  4. 订单系统:创建订单、支付、订单管理
  5. 搜索功能:根据关键词搜索商品
  6. 分类浏览:根据分类浏览商品

技术栈选择

  • 前端框架:Nuxt.js 3
  • 状态管理:Pinia
  • HTTP客户端:Axios
  • UI组件库:Element Plus
  • CSS预处理器:SCSS
  • 图标库:Font Awesome
  • 支付集成:Stripe

技术方案设计

项目结构设计

nuxt-ecommerce/
├── assets/            # 静态资源
│   ├── css/           # 样式文件
│   ├── images/        # 图片资源
│   └── fonts/         # 字体文件
├── components/        # 组件
│   ├── common/        # 通用组件
│   ├── layout/        # 布局组件
│   ├── product/       # 商品相关组件
│   ├── user/          # 用户相关组件
│   └── cart/          # 购物车相关组件
├── composables/       # 组合式API
├── pages/             # 页面
│   ├── index.vue      # 首页
│   ├── products/       # 商品相关页面
│   │   ├── index.vue  # 商品列表
│   │   └── _id.vue    # 商品详情
│   ├── categories/    # 分类相关页面
│   │   └── _id.vue    # 分类商品列表
│   ├── cart.vue       # 购物车页面
│   ├── checkout.vue   # 结算页面
│   ├── orders/        # 订单相关页面
│   │   ├── index.vue  # 订单列表
│   │   └── _id.vue    # 订单详情
│   └── user/          # 用户相关页面
│       ├── login.vue  # 登录页面
│       ├── register.vue # 注册页面
│       └── profile.vue # 个人中心
├── plugins/           # 插件
├── stores/            # 状态管理
│   ├── product.js     # 商品状态
│   ├── cart.js        # 购物车状态
│   ├── user.js        # 用户状态
│   └── order.js       # 订单状态
├── server/            # 服务器端
│   ├── api/           # API路由
│   └── middleware/    # 服务器中间件
├── static/            # 静态文件
├── utils/             # 工具函数
├── nuxt.config.js     # 配置文件
├── package.json       # 项目依赖
└── README.md          # 项目说明

数据结构设计

  1. 商品数据结构
{
  id: Number,           // 商品ID
  name: String,         // 商品名称
description: String,  // 商品描述
  price: Number,        // 商品价格
  originalPrice: Number, // 原价
  discount: Number,     // 折扣
  stock: Number,        // 库存
  categoryId: Number,   // 分类ID
  images: Array,        // 商品图片
  attributes: Object,   // 商品属性
  createdAt: Date,      // 创建时间
  updatedAt: Date       // 更新时间
}
  1. 用户数据结构
{
  id: Number,           // 用户ID
  username: String,     // 用户名
  email: String,        // 邮箱
  password: String,     // 密码(加密存储)
  firstName: String,    // 名字
  lastName: String,     // 姓氏
  address: Object,      // 地址
  phone: String,        // 电话
  createdAt: Date,      // 创建时间
  updatedAt: Date       // 更新时间
}
  1. 购物车数据结构
{
  id: Number,           // 购物车项ID
  userId: Number,       // 用户ID
  productId: Number,    // 商品ID
  quantity: Number,     // 数量
  product: Object,      // 商品信息
  createdAt: Date,      // 创建时间
  updatedAt: Date       // 更新时间
}
  1. 订单数据结构
{
  id: Number,           // 订单ID
  userId: Number,       // 用户ID
  items: Array,         // 订单商品
  totalPrice: Number,   // 总价格
  status: String,       // 订单状态
  paymentMethod: String, // 支付方式
  shippingAddress: Object, // 收货地址
  createdAt: Date,      // 创建时间
  updatedAt: Date       // 更新时间
}

数据获取和状态管理

数据获取

使用Nuxt.js的useAsyncDatauseFetch组合式API进行数据获取:

// pages/products/index.vue
<script setup>
const { data: products, pending, error } = useAsyncData('products', () => {
  return $fetch('/api/products')
})
</script>

状态管理

使用Pinia进行状态管理:

// stores/product.js
import { defineStore } from 'pinia'

export const useProductStore = defineStore('product', {
  state: () => ({
    products: [],
    currentProduct: null,
    loading: false,
    error: null
  }),
  actions: {
    async fetchProducts() {
      this.loading = true
      try {
        this.products = await $fetch('/api/products')
        this.error = null
      } catch (err) {
        this.error = err
      } finally {
        this.loading = false
      }
    },
    async fetchProduct(id) {
      this.loading = true
      try {
        this.currentProduct = await $fetch(`/api/products/${id}`)
        this.error = null
      } catch (err) {
        this.error = err
      } finally {
        this.loading = false
      }
    }
  }
})

路由和导航

路由配置

使用Nuxt.js的基于文件系统的路由:

  1. 商品列表页面pages/products/index.vue
  2. 商品详情页面pages/products/_id.vue
  3. 分类商品页面pages/categories/_id.vue
  4. 购物车页面pages/cart.vue
  5. 结算页面pages/checkout.vue
  6. 订单列表页面pages/orders/index.vue
  7. 订单详情页面pages/orders/_id.vue
  8. 登录页面pages/user/login.vue
  9. 注册页面pages/user/register.vue
  10. 个人中心页面pages/user/profile.vue

导航守卫

使用Nuxt.js的中间件实现导航守卫:

// middleware/auth.js
export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()
  
  if (!userStore.isAuthenticated) {
    return navigateTo('/user/login')
  }
})

在需要认证的页面中使用:

<!-- pages/user/profile.vue -->
<template>
  <div class="profile">
    <!-- 个人中心内容 -->
  </div>
</template>

<script setup>
// 使用认证中间件
definePageMeta({
  middleware: 'auth'
})
</script>

错误处理和国际化

错误处理

  1. 全局错误处理

plugins/error-handler.js中配置全局错误处理:

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('vue:error', (err, instance, info) => {
    console.error('Vue error:', err)
    // 可以在这里添加错误监控逻辑
  })
  
  process.on('unhandledRejection', (err) => {
    console.error('Unhandled rejection:', err)
  })
  
  process.on('uncaughtException', (err) => {
    console.error('Uncaught exception:', err)
  })
})
  1. 页面级错误处理

使用Nuxt.js的错误边界:

<!-- pages/_error.vue -->
<template>
  <div class="error-page">
    <h1 v-if="error.statusCode === 404">页面不存在</h1>
    <h1 v-else>服务器错误</h1>
    <p>{{ error.message }}</p>
    <NuxtLink to="/">返回首页</NuxtLink>
  </div>
</template>

<script setup>
defineProps({
  error: {
    type: Object,
    required: true
  }
})
</script>

国际化

使用Nuxt.js的i18n模块实现国际化:

  1. 安装依赖
npm install @nuxtjs/i18n
  1. 配置i18n

nuxt.config.js中添加:

export default {
  modules: [
    '@nuxtjs/i18n'
  ],
  i18n: {
    locales: [
      {
        code: 'en',
        name: 'English',
        file: 'en.json'
      },
      {
        code: 'zh',
        name: '中文',
        file: 'zh.json'
      }
    ],
    defaultLocale: 'en',
    lazy: true,
    langDir: 'locales/'
  }
}
  1. 创建语言文件

创建locales/en.jsonlocales/zh.json文件:

// locales/en.json
{
  "welcome": "Welcome to our store",
  "products": "Products",
  "cart": "Cart",
  "login": "Login",
  "register": "Register"
}
// locales/zh.json
{
  "welcome": "欢迎来到我们的商店",
  "products": "商品",
  "cart": "购物车",
  "login": "登录",
  "register": "注册"
}
  1. 使用国际化

在组件中使用:

<template>
  <div>
    <h1>{{ $t('welcome') }}</h1>
    <NuxtLink to="/products">{{ $t('products') }}</NuxtLink>
  </div>
</template>

性能优化和部署

性能优化

  1. 代码分割

使用Nuxt.js的自动代码分割功能:

// nuxt.config.js
export default {
  build: {
    splitChunks: {
      layouts: true,
      pages: true,
      commons: true
    }
  }
}
  1. 图片优化

使用Nuxt.js的图片组件:

<template>
  <NuxtImg src="/products/1.jpg" alt="Product image" width="300" height="300" />
</template>
  1. 缓存策略

配置HTTP缓存:

// server/middleware/cache.js
export default defineEventHandler((event) => {
  // 设置静态资源缓存
  if (event.node.req.url.startsWith('/_nuxt/')) {
    event.node.res.setHeader('Cache-Control', 'max-age=31536000, immutable')
  }
})

部署

  1. 静态生成部署
npm run generate
  1. 服务端渲染部署
npm run build
npm run start
  1. 容器化部署

创建Dockerfile

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "start"]

实用案例分析

商品详情页面实现

功能需求

  • 展示商品基本信息
  • 展示商品图片
  • 展示商品属性和价格
  • 添加到购物车功能
  • 相关商品推荐

实现步骤

  1. 创建商品详情页面
<!-- pages/products/_id.vue -->
<template>
  <div class="product-detail">
    <div v-if="pending">加载中...</div>
    <div v-else-if="error">{{ error.message }}</div>
    <div v-else-if="product">
      <div class="product-images">
        <NuxtImg 
          v-for="(image, index) in product.images" 
          :key="index" 
          :src="image" 
          :alt="product.name" 
          class="product-image"
        />
      </div>
      <div class="product-info">
        <h1>{{ product.name }}</h1>
        <div class="product-price">
          <span class="current-price">{{ product.price }}</span>
          <span v-if="product.originalPrice" class="original-price">{{ product.originalPrice }}</span>
        </div>
        <div class="product-description">
          <p>{{ product.description }}</p>
        </div>
        <div class="product-attributes">
          <!-- 商品属性 -->
        </div>
        <div class="product-actions">
          <button 
            class="add-to-cart" 
            @click="addToCart(product)"
          >
            添加到购物车
          </button>
        </div>
      </div>
      <div class="related-products">
        <h2>相关商品</h2>
        <!-- 相关商品列表 -->
      </div>
    </div>
  </div>
</template>

<script setup>
const route = useRoute()
const productStore = useProductStore()
const cartStore = useCartStore()

const { data: product, pending, error } = useAsyncData('product', () => {
  return $fetch(`/api/products/${route.params.id}`)
})

const addToCart = (product) => {
  cartStore.addToCart(product)
}
</script>
  1. 实现添加到购物车功能
// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    loading: false,
    error: null
  }),
  getters: {
    totalItems: (state) => {
      return state.items.reduce((total, item) => total + item.quantity, 0)
    },
    totalPrice: (state) => {
      return state.items.reduce((total, item) => {
        return total + (item.product.price * item.quantity)
      }, 0)
    }
  },
  actions: {
    addToCart(product) {
      const existingItem = this.items.find(item => item.product.id === product.id)
      
      if (existingItem) {
        existingItem.quantity++
      } else {
        this.items.push({
          product,
          quantity: 1
        })
      }
      
      // 保存到本地存储
      localStorage.setItem('cart', JSON.stringify(this.items))
    },
    removeFromCart(productId) {
      this.items = this.items.filter(item => item.product.id !== productId)
      localStorage.setItem('cart', JSON.stringify(this.items))
    },
    updateQuantity(productId, quantity) {
      const item = this.items.find(item => item.product.id === productId)
      if (item) {
        item.quantity = quantity
        localStorage.setItem('cart', JSON.stringify(this.items))
      }
    },
    clearCart() {
      this.items = []
      localStorage.removeItem('cart')
    },
    loadCart() {
      const savedCart = localStorage.getItem('cart')
      if (savedCart) {
        this.items = JSON.parse(savedCart)
      }
    }
  }
})

购物车页面实现

功能需求

  • 展示购物车商品列表
  • 修改商品数量
  • 删除购物车商品
  • 显示购物车总价
  • 跳转到结算页面

实现步骤

  1. 创建购物车页面
<!-- pages/cart.vue -->
<template>
  <div class="cart">
    <h1>购物车</h1>
    <div v-if="cartStore.items.length === 0" class="empty-cart">
      <p>购物车为空</p>
      <NuxtLink to="/products">去购物</NuxtLink>
    </div>
    <div v-else>
      <div class="cart-items">
        <div v-for="item in cartStore.items" :key="item.product.id" class="cart-item">
          <div class="item-image">
            <NuxtImg :src="item.product.images[0]" :alt="item.product.name" />
          </div>
          <div class="item-info">
            <h3>{{ item.product.name }}</h3>
            <p class="item-price">{{ item.product.price }}</p>
          </div>
          <div class="item-quantity">
            <button @click="cartStore.updateQuantity(item.product.id, item.quantity - 1)" :disabled="item.quantity <= 1">-</button>
            <span>{{ item.quantity }}</span>
            <button @click="cartStore.updateQuantity(item.product.id, item.quantity + 1)">+</button>
          </div>
          <div class="item-total">
            {{ item.product.price * item.quantity }}
          </div>
          <div class="item-remove">
            <button @click="cartStore.removeFromCart(item.product.id)">删除</button>
          </div>
        </div>
      </div>
      <div class="cart-summary">
        <h2>购物车总计</h2>
        <div class="summary-item">
          <span>商品数量</span>
          <span>{{ cartStore.totalItems }}</span>
        </div>
        <div class="summary-item">
          <span>商品总价</span>
          <span>{{ cartStore.totalPrice }}</span>
        </div>
        <div class="summary-item total">
          <span>总计</span>
          <span>{{ cartStore.totalPrice }}</span>
        </div>
        <NuxtLink to="/checkout" class="checkout-btn">去结算</NuxtLink>
      </div>
    </div>
  </div>
</template>

<script setup>
const cartStore = useCartStore()

// 加载购物车数据
onMounted(() => {
  cartStore.loadCart()
})
</script>

总结

本章节通过一个电商网站项目实战,巩固了Nuxt.js的核心功能知识点:

  1. 项目需求分析:明确项目功能和目标,选择合适的技术栈。
  2. 技术方案设计:设计项目结构和数据结构,为开发做好准备。
  3. 数据获取和状态管理:使用useAsyncDatauseFetch进行数据获取,使用Pinia进行状态管理。
  4. 路由和导航:使用Nuxt.js的基于文件系统的路由,实现导航守卫。
  5. 错误处理和国际化:实现全局和页面级错误处理,配置国际化支持。
  6. 性能优化和部署:实现代码分割、图片优化和缓存策略,配置不同的部署方式。

通过本章节的学习,你应该能够独立开发一个功能完整的Nuxt.js应用,并掌握项目开发的流程和最佳实践。

« 上一篇 Nuxt.js部署策略 下一篇 » 组合式API在Nuxt.js中的应用