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
- usingVitecreationVue 3project
- installation并configurationVue Router, Pinia, Element Plus and Axios
- creationproject Basicsstructure and 公共component
- implementation商品list, 商品详情, 购物车, login/registeretc.页面
- 添加routing守卫 and 表单verification
- 构建produceversion并deployment to server