Vueproject实战

through完整 实战projectLearningVue.jsDevelopment流程, from projectplanning to deployment on 线

1. projectplanning

in 开始编码之 before , 我们需要 for projectforplanning, includingprojectrequirementsanalysis, techniques栈选择, projectstructuredesignetc..

1.1 projectrequirements

我们将creation一个 simple 电商网站, 主要functionsincluding:

  • 商品list展示
  • 商品详情查看
  • 购物车functions
  • userlogin/register
  • 订单management

1.2 techniques栈选择

techniques version 用途
Vue.js 3.x before 端framework
Vue Router 4.x routingmanagement
Pinia 2.x statusmanagement
Axios 1.x HTTP客户端
Element Plus 2.x UIcomponentlibrary
Vite 4.x 构建tool

2. project初始化

usingVitecreationVue 3project, 并installation所需依赖.

2.1 creationproject

# creationVue 3project
npm create vite@latest vue-shop -- --template vue

# 进入projectTable of Contents
cd vue-shop

2.2 installation依赖

# installationcore依赖
npm install vue-router@4 pinia axios element-plus

# installationDevelopment依赖
npm install -D sass

2.3 projectstructuredesign

vue-shop/
├── public/               # 静态resource
├── src/                  # sourcescode
│   ├── assets/           # resourcefile
│   ├── components/       # 公共component
│   ├── views/            # 页面component
│   ├── router/           # routingconfiguration
│   ├── stores/           # Piniastatusmanagement
│   ├── services/         # APIservice
│   ├── utils/            # toolfunction
│   ├── App.vue           # 根component
│   └── main.js           # 入口file
├── vite.config.js        # Viteconfiguration
├── package.json          # projectconfiguration
└── README.md             # project说明

3. Basicsconfiguration

configurationVue Router, Pinia and Element Plus.

3.1 configurationVue Router

creationsrc/router/index.jsfile:

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/product/:id',
    name: 'ProductDetail',
    component: () => import('../views/ProductDetail.vue')
  },
  {
    path: '/cart',
    name: 'Cart',
    component: () => import('../views/Cart.vue')
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue')
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import('../views/Register.vue')
  },
  {
    path: '/orders',
    name: 'Orders',
    component: () => import('../views/Orders.vue')
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router

3.2 configurationPinia

creationsrc/stores/index.jsfile:

import { createPinia } from 'pinia'

const pinia = createPinia()

export default pinia

3.3 configurationElement Plus

modifysrc/main.jsfile:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(router)
app.use(pinia)
app.use(ElementPlus)

app.mount('#app')

4. componentDevelopment

开始Developmentproject corecomponent.

4.1 公共component

creationsrc/components/Header.vuecomponent:

<template>
  <header class="header">
    <div class="container">
      <div class="header-content">
        <div class="logo">
          <router-link to="/">Vue Shop</router-link>
        </div>
        
        <nav class="nav">
          <router-link to="/">首页</router-link>
          <router-link to="/cart">购物车</router-link>
          <router-link to="/orders">订单</router-link>
        </nav>
        
        <div class="user-info">
          <el-button v-if="!isLoggedIn" type="primary" @click="handleLogin">
            login
          </el-button>
          <el-dropdown v-else>
            <span class="user-name">
              {{ user.name }} <i class="el-icon-arrow-down"></i>
            </span>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item @click="handleLogout">退出login</el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </div>
      </div>
    </div>
  </header>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '../stores/user'

const router = useRouter()
const userStore = useUserStore()

const isLoggedIn = computed(() => userStore.isLoggedIn)
const user = computed(() => userStore.user)

const handleLogin = () => {
  router.push('/login')
}

const handleLogout = () => {
  userStore.logout()
  router.push('/')
}
</script>

<style scoped>
.header {
  background-color: #4285f4;
  color: white;
  padding: 15px 0;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.header-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

.logo a {
  color: white;
  font-size: 1.8rem;
  font-weight: bold;
  text-decoration: none;
}

.nav {
  display: flex;
  gap: 20px;
}

.nav a {
  color: white;
  text-decoration: none;
  font-size: 1rem;
  transition: color 0.3s ease;
}

.nav a:hover {
  color: #e8f0fe;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 10px;
}

.user-name {
  color: white;
  cursor: pointer;
  font-size: 1rem;
}
</style>

4.2 页面component

creationsrc/views/Home.vue页面:

<template>
  <div class="home">
    <h2>商品list</h2>
    <div class="product-list">
      <el-card
        v-for="product in products"
        :key="product.id"
        class="product-card"
        @click="goToDetail(product.id)"
      >
        <img
          slot="header"
          :src="product.image"
          :alt="product.name"
          class="product-image"
        >
        <div class="product-info">
          <h3 class="product-name">{{ product.name }}</h3>
          <p class="product-price">¥{{ product.price }}</p>
          <el-button type="primary" @click.stop="addToCart(product)">
            加入购物车
          </el-button>
        </div>
      </el-card>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useProductStore } from '../stores/product'
import { useCartStore } from '../stores/cart'

const router = useRouter()
const productStore = useProductStore()
const cartStore = useCartStore()

const products = ref([])

const goToDetail = (id) => {
  router.push(`/product/${id}`)
}

const addToCart = (product) => {
  cartStore.addToCart(product)
  ElMessage.success('已添加 to 购物车')
}

onMounted(async () => {
  products.value = await productStore.fetchProducts()
})
</script>

<style scoped>
.home {
  padding: 20px;
}

.product-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

.product-card {
  cursor: pointer;
  transition: transform 0.3s ease;
}

.product-card:hover {
  transform: translateY(-5px);
}

.product-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.product-info {
  text-align: center;
}

.product-name {
  font-size: 1.2rem;
  margin: 10px 0;
  color: #333;
}

.product-price {
  font-size: 1.3rem;
  color: #e74c3c;
  font-weight: bold;
  margin: 10px 0;
}
</style>

5. statusmanagement

usingPiniamanagementapplicationstatus.

5.1 产品status

creationsrc/stores/product.jsfile:

import { defineStore } from 'pinia'
import { api } from '../services/api'

export const useProductStore = defineStore('product', {
  state: () => ({
    products: [],
    loading: false
  }),
  
  actions: {
    async fetchProducts() {
      this.loading = true
      try {
        const response = await api.get('/products')
        this.products = response.data
        return response.data
      } catch (error) {
        console.error('Failed to fetch products:', error)
        return []
      } finally {
        this.loading = false
      }
    },
    
    async fetchProductById(id) {
      this.loading = true
      try {
        const response = await api.get(`/products/${id}`)
        return response.data
      } catch (error) {
        console.error(`Failed to fetch product ${id}:`, error)
        return null
      } finally {
        this.loading = false
      }
    }
  }
})

5.2 购物车status

creationsrc/stores/cart.jsfile:

import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: JSON.parse(localStorage.getItem('cart')) || []
  }),
  
  getters: {
    totalItems: (state) => state.items.reduce((total, item) => total + item.quantity, 0),
    totalPrice: (state) => state.items.reduce((total, item) => total + item.price * item.quantity, 0)
  },
  
  actions: {
    addToCart(product) {
      const existingItem = this.items.find(item => item.id === product.id)
      
      if (existingItem) {
        existingItem.quantity += 1
      } else {
        this.items.push({
          ...product,
          quantity: 1
        })
      }
      
      this.saveToLocalStorage()
    },
    
    removeFromCart(productId) {
      this.items = this.items.filter(item => item.id !== productId)
      this.saveToLocalStorage()
    },
    
    updateQuantity(productId, quantity) {
      const item = this.items.find(item => item.id === productId)
      if (item) {
        item.quantity = quantity
        this.saveToLocalStorage()
      }
    },
    
    clearCart() {
      this.items = []
      this.saveToLocalStorage()
    },
    
    saveToLocalStorage() {
      localStorage.setItem('cart', JSON.stringify(this.items))
    }
  }
})

6. APIservice

creationAPIservice层, processing and after 端 通信.

6.1 creationAPIinstance

creationsrc/services/api.jsfile:

import axios from 'axios'

// creationaxiosinstance
const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// request拦截器
api.interceptors.request.use(
  (config) => {
    // 添加token to request头
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// response拦截器
api.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    if (error.response?.status === 401) {
      // processing未authorizationerror
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export { api }

6.2 mockAPIdata

creationsrc/services/mockData.jsfile, 用于Development阶段 mockdata:

export const mockProducts = [
  {
    id: 1,
    name: 'Vue 3 实战tutorial',
    price: 99,
    image: 'https://via.placeholder.com/280x200?text=Vue+3+Book',
    description: '全面LearningVue 3 实战tutorial, package含组合式API, Piniaetc. new features. '
  },
  {
    id: 2,
    name: 'JavaScriptadvancedprogramming',
    price: 128,
    image: 'https://via.placeholder.com/280x200?text=JS+Book',
    description: '深入understandingJavaScript advanced features and design模式. '
  },
  {
    id: 3,
    name: ' before 端performanceoptimizationguide',
    price: 88,
    image: 'https://via.placeholder.com/280x200?text=Performance+Book',
    description: ' before 端performanceoptimization best practices and tool介绍. '
  }
]

export const mockUsers = [
  {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    password: '123456'
  }
]

7. routing守卫

添加routing守卫, 保护需要login 页面.

// modifysrc/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  // routingconfiguration...
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// routing守卫
router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token') !== null
  
  // 需要login 页面
  const requiresAuth = ['Cart', 'Orders'].includes(to.name)
  
  if (requiresAuth && !isLoggedIn) {
    next('/login')
  } else {
    next()
  }
})

export default router

8. 表单verification

implementationlogin and register表单 verification.

<template>
  <div class="login">
    <h2>userlogin</h2>
    <el-form
      ref="loginFormRef"
      :model="loginForm"
      :rules="loginRules"
      label-position="top"
      class="login-form"
    >
      <el-form-item label="邮箱" prop="email">
        <el-input v-model="loginForm.email" placeholder="请输入邮箱" />
      </el-form-item>
      
      <el-form-item label="password" prop="password">
        <el-input
          v-model="loginForm.password"
          type="password"
          placeholder="请输入password"
          show-password
        />
      </el-form-item>
      
      <el-form-item>
        <el-button type="primary" @click="handleLogin" :loading="loading">
          login
        </el-button>
        <el-button @click="goToRegister">register new 账号</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '../stores/user'

const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref(null)
const loading = ref(false)

const loginForm = reactive({
  email: '',
  password: ''
})

const loginRules = {
  email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { type: 'email', message: '请输入正确 邮箱格式', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入password', trigger: 'blur' },
    { min: 6, message: 'password long 度不能 few 于6个字符', trigger: 'blur' }
  ]
}

const handleLogin = async () => {
  if (!loginFormRef.value) return
  
  try {
    await loginFormRef.value.validate()
    loading.value = true
    
    const success = await userStore.login(loginForm)
    if (success) {
      ElMessage.success('login成功')
      router.push('/')
    } else {
      ElMessage.error('邮箱 or passworderror')
    }
  } catch (error) {
    console.error('Login validation failed:', error)
  } finally {
    loading.value = false
  }
}

const goToRegister = () => {
  router.push('/register')
}
</script>

<style scoped>
.login {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
}

.login-form {
  margin-top: 20px;
}
</style>

9. project构建 and deployment

构建produceversion并deployment to server.

9.1 构建project

# 构建produceversion
npm run build

9.2 deployment to Nginx

将构建 after distTable of Contents on 传 to server, 并configurationNginx:

server {
    listen 80;
    server_name your-domain.com;
    root /path/to/your/dist;
    index index.html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    location /api {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

9.3 environmentvariableconfiguration

creation.env.productionfile, configurationproduceenvironmentvariable:

VITE_API_BASE_URL=https://api.your-domain.com

10. best practices

in VueprojectDevelopmentin, 我们应该遵循以 under best practices:

  • component化Development: 将UI拆分 for 可复用 component
  • 单一职责principles: 每个component/function只负责一件事
  • statusmanagement: usingPiniamanagement全局status
  • routing守卫: 保护需要authentication routing
  • 表单verification: usingElement Plus 表单verificationfunctions
  • errorprocessing: 统一processingAPIerror
  • response式design: 确保 in 不同设备 on 都能良 good 显示
  • code规范: usingESLint and Prettier规范code
  • performanceoptimization: usingv-memo, v-onceetc.指令optimizationperformance
  • documentationwriting: for component and functionwritingdocumentation

练习: creation完整 Vue电商project

  1. usingVitecreationVue 3project
  2. installation并configurationVue Router, Pinia, Element Plus and Axios
  3. creationproject Basicsstructure and 公共component
  4. implementation商品list, 商品详情, 购物车, login/registeretc.页面
  5. 添加routing守卫 and 表单verification
  6. 构建produceversion并deployment to server