项目实战是Vue学习的关键环节,能帮助初学者将零散的基础知识点整合应用,提升工程化实践能力。本文以“电商商品列表页”为实战案例,详细讲解从需求分析、项目搭建、核心功能实现到测试优化的完整流程,提供可直接落地的代码示例与思路。
一、需求分析:明确核心功能与边界
电商商品列表页是用户浏览商品的核心入口,需聚焦核心功能,避免过度设计。明确以下核心需求:
- 商品列表渲染:展示商品核心信息(图片、名称、价格、销量),布局清晰直观;
- 条件筛选:支持按价格区间(最低-最高价格)、商品分类(如手机数码、服装鞋帽)筛选;
- 排序功能:提供多维度排序选项(默认、价格升序、价格降序、销量排序);
- 分页功能:展示分页控件,支持页码切换、每页条数调整,显示商品总数;
- 商品跳转:点击商品卡片,跳转到对应的商品详情页(携带商品ID参数)。
二、技术栈选择:轻量高效,适配入门实战
选择成熟、易上手的技术栈,降低实战门槛,同时覆盖Vue核心生态:
- 前端框架:Vue3 + Vite(Vue3语法更简洁,Vite构建速度快,适合入门);
- 路由管理:Vue Router 4(实现商品列表页与详情页的路由跳转、参数传递);
- UI组件库:Element Plus(快速实现筛选表单、分页、卡片等组件,提升开发效率);
- 接口交互:Axios(封装请求,模拟商品数据获取,对接后端接口);
- 状态管理:Pinia(轻量易用,管理筛选条件、商品列表、分页信息等全局状态)。
三、项目搭建:分步骤初始化,规范目录结构
按“创建项目→安装依赖→设计目录”的步骤初始化,确保项目结构清晰,符合工程化规范。
1. 创建Vite + Vue3项目
npm create vite@latest vue-shop-list -- --template vue cd vue-shop-list # 进入项目目录 npm install # 安装基础依赖
2. 安装核心依赖包
npm install vue-router@4 element-plus axios pinia
3. 规范项目目录设计
按功能模块划分目录,便于后续维护与扩展:
src/ ├── api/ # 接口请求封装(按业务模块拆分) │ └── goods.js # 商品相关接口(列表查询等) ├── components/ # 公共可复用组件 │ ├── GoodsCard.vue # 商品卡片组件(单独渲染单个商品) │ ├── FilterPanel.vue # 筛选面板组件(价格、分类筛选) │ └── Pagination.vue # 分页组件(复用分页逻辑) ├── router/ # 路由配置目录 │ └── index.js # 路由规则定义(列表页、详情页) ├── stores/ # Pinia状态管理目录 │ └── goodsStore.js # 商品相关状态(筛选、列表、分页) ├── views/ # 页面级组件(视图) │ ├── GoodsList.vue # 商品列表页(整合各组件) │ └── GoodsDetail.vue # 商品详情页(接收商品ID) ├── App.vue # 根组件(挂载路由出口) └── main.js # 入口文件(初始化Vue、路由、Pinia等)
四、核心功能实现:分模块编码,聚焦组件化与状态管理
按“路由→状态→接口→组件”的顺序实现,确保各模块解耦,数据流转清晰。
1. 路由配置:实现页面跳转与参数传递
配置商品列表页与详情页路由,支持通过参数传递商品ID:
// router/index.js import { createRouter, createWebHistory } from 'vue-router'; import GoodsList from '../views/GoodsList.vue'; // 商品列表页 import GoodsDetail from '../views/GoodsDetail.vue'; // 商品详情页 // 路由规则 const routes = [ { path: '/', redirect: '/goods-list' }, // 根路径重定向到列表页 { path: '/goods-list', component: GoodsList, name: 'GoodsList' // 命名路由,便于跳转 }, { path: '/goods-detail/:id', // 动态路由,接收商品ID component: GoodsDetail, name: 'GoodsDetail' } ]; // 创建路由实例 const router = createRouter({ history: createWebHistory(), // HTML5 history模式(无#) routes }); export default router;
2. 状态管理:用Pinia统一管理全局状态
将筛选条件、商品列表、分页信息等全局状态放入Pinia,实现组件间数据共享:
// stores/goodsStore.js import { defineStore } from 'pinia'; import { getGoodsList } from '../api/goods'; // 引入商品列表接口 // 定义并导出商品相关Store export const useGoodsStore = defineStore('goods', { state: () => ({ goodsList: [], // 商品列表数据 total: 0, // 商品总数(用于分页) pageSize: 10, // 每页显示条数 currentPage: 1, // 当前页码 filterParams: { // 筛选参数 minPrice: '', // 最低价格 maxPrice: '', // 最高价格 categoryId: ''// 商品分类ID }, sortRule: 'default' // 排序规则:default/price-asc/price-desc/sales }), actions: { // 更新筛选参数:接收新参数,重置页码并重新获取列表 updateFilterParams(params) { this.filterParams = { ...this.filterParams, ...params }; this.currentPage = 1; // 筛选条件变化,回到第一页 this.fetchGoodsList(); // 重新请求商品列表 }, // 更新排序规则:切换排序后重新获取列表 updateSortRule(rule) { this.sortRule = rule; this.fetchGoodsList(); }, // 更新当前页码:切换页码后重新获取列表 updateCurrentPage(page) { this.currentPage = page; this.fetchGoodsList(); }, // 核心动作:获取商品列表数据(调用接口,更新状态) async fetchGoodsList() { try { // 调用接口,传递分页、筛选、排序参数 const res = await getGoodsList({ page: this.currentPage, pageSize: this.pageSize, ...this.filterParams, // 展开筛选参数 sort: this.sortRule // 排序规则 }); // 更新状态:商品列表与总数 this.goodsList = res.data.list; this.total = res.data.total; } catch (error) { console.error('获取商品列表失败:', error); // 可添加错误提示(如Element Plus的Message组件) } } } });
3. 接口封装:统一请求逻辑,模拟数据交互
封装Axios请求,统一基础路径、超时时间,模拟商品列表接口(实际项目替换为真实后端地址):
// api/goods.js import axios from 'axios'; // 创建Axios实例,统一配置 const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/mock', // 从环境变量读取基础地址,默认mock timeout: 5000 // 超时时间5秒 }); // 封装商品列表查询接口(暴露给外部调用) export const getGoodsList = (params) => { return request.get('/goods/list', { params }); // GET请求,参数拼接到URL }; // 后续可扩展其他接口(如商品详情查询)
4. 组件实现:拆分公共组件,实现复用与通信
按功能拆分组件,通过Props传递数据、自定义事件触发动作,实现组件解耦。
(1)筛选面板组件(FilterPanel.vue):负责筛选条件输入与提交
接收初始筛选参数,用户输入后触发筛选事件,将参数传递给父组件:
<template> <div class="filter-panel"> <!-- 价格区间筛选 --> <el-input-number v-model="filterParams.minPrice" label="最低价格" placeholder="请输入最低价格" :min="0" // 价格不能为负 ></el-input-number> <span>-</span> <el-input-number v-model="filterParams.maxPrice" label="最高价格" placeholder="请输入最高价格" :min="filterParams.minPrice || 0" // 最高价格不低于最低价格 ></el-input-number> <!-- 商品分类筛选 --> <el-select v-model="filterParams.categoryId" placeholder="请选择商品分类" style="margin-left: 10px;" > <el-option label="全部商品" value=""></el-option> <el-option label="手机数码" value="1"></el-option> <el-option label="服装鞋帽" value="2"></el-option> </el-select> <!-- 筛选按钮:触发筛选事件 --> <el-button type="primary" @click="handleFilter" style="margin-left: 10px;">筛选</el-button> </div> </template> <script setup> import { defineProps, defineEmits, ref } from 'vue'; // 接收父组件传递的初始筛选参数 const props = defineProps({ initialParams: { type: Object, default: () => ({}) } }); // 定义筛选事件,向父组件传递筛选参数 const emit = defineEmits(['filter']); // 本地维护筛选参数(避免直接修改Props) const filterParams = ref({ ...props.initialParams }); // 筛选按钮点击事件:触发父组件筛选动作 const handleFilter = () => { emit('filter', { ...filterParams.value }); }; </script>
(2)商品卡片组件(GoodsCard.vue):单独渲染单个商品,支持跳转
接收单个商品数据,渲染卡片样式,点击后跳转到详情页:
<template> <el-card class="goods-card" @click="goToDetail" hover> <!-- 商品图片 --> <img :src="goods.imgUrl" alt="商品图片" class="goods-img"> <!-- 商品名称 --> <h3 class="goods-name">{{ goods.name }}</h3> <!-- 商品价格(保留2位小数) --> <p class="goods-price">¥{{ goods.price.toFixed(2) }}</p> <!-- 商品销量 --> <p class="goods-sales">销量:{{ goods.sales }}</p> </el-card> </template> <script setup> import { defineProps } from 'vue'; import { useRouter } from 'vue-router'; // 引入路由钩子 // 接收父组件传递的单个商品数据 const props = defineProps({ goods: { type: Object, required: true, default: () => ({}) } }); // 获取路由实例,用于跳转 const router = useRouter(); // 点击卡片跳转详情页:传递商品ID const goToDetail = () => { router.push({ name: 'GoodsDetail', // 命名路由跳转 params: { id: props.goods.id } // 传递商品ID参数 }); }; </script>
(3)商品列表页(GoodsList.vue):整合所有组件,实现功能联动
作为父组件,整合筛选面板、排序栏、商品列表、分页组件,通过Pinia实现数据联动:
<template> <div class="goods-list-page"> <h1>商品列表</h1> <!-- 筛选面板:传递初始参数,监听筛选事件 --> <FilterPanel :initial-params="goodsStore.filterParams" @filter="goodsStore.updateFilterParams" ></FilterPanel> <!-- 排序栏:切换排序规则 --> <div class="sort-bar"> <el-button :type="goodsStore.sortRule === 'default' ? 'primary' : ''" @click="goodsStore.updateSortRule('default')" >默认排序</el-button> <el-button :type="goodsStore.sortRule === 'price-asc' ? 'primary' : ''" @click="goodsStore.updateSortRule('price-asc')" >价格升序</el-button> <el-button :type="goodsStore.sortRule === 'price-desc' ? 'primary' : ''" @click="goodsStore.updateSortRule('price-desc')" >价格降序</el-button> <el-button :type="goodsStore.sortRule === 'sales' ? 'primary' : ''" @click="goodsStore.updateSortRule('sales')" >销量排序</el-button> </div> <!-- 商品列表:循环渲染商品卡片 --> <div class="goods-list"> <GoodsCard v-for="goods in goodsStore.goodsList" :key="goods.id" // 唯一标识,避免重复渲染 :goods="goods" // 传递单个商品数据 ></GoodsCard> </div> <!-- 分页控件:监听页码、每页条数变化 --> <el-pagination @size-change="handleSizeChange" // 每页条数变化 @current-change="goodsStore.updateCurrentPage" // 页码变化 :current-page="goodsStore.currentPage" // 当前页码 :page-sizes="[5, 10, 20]" // 可选每页条数 :page-size="goodsStore.pageSize" // 当前每页条数 layout="total, sizes, prev, pager, next, jumper" // 分页布局 :total="goodsStore.total" // 商品总数 style="margin-top: 20px;" ></el-pagination> </div> </template> <script setup> import { onMounted } from 'vue'; import { useGoodsStore } from '../stores/goodsStore'; // 引入商品Store import FilterPanel from '../components/FilterPanel.vue'; // 筛选面板组件 import GoodsCard from '../components/GoodsCard.vue'; // 商品卡片组件 // 获取Pinia状态实例 const goodsStore = useGoodsStore(); // 页面挂载时,初始化获取商品列表 onMounted(() => { goodsStore.fetchGoodsList(); }); // 处理每页条数变化:更新条数后重置页码,重新获取列表 const handleSizeChange = (pageSize) => { goodsStore.pageSize = pageSize; goodsStore.currentPage = 1; goodsStore.fetchGoodsList(); }; </script>
五、测试与优化:保障功能稳定,提升用户体验
功能实现后,需通过测试验证完整性,并进行基础优化。
- 功能测试:逐一验证核心功能——筛选条件是否生效、排序是否正确、分页切换是否流畅、卡片跳转是否携带正确ID;
- 样式优化:调整组件间距、卡片布局,使用弹性布局(flex)确保在不同屏幕尺寸下自适应显示,提升视觉体验;
- 性能优化:① 用v-once指令优化商品名称、价格等静态内容的渲染(仅渲染一次);② 用keep-alive缓存筛选面板组件,减少页面切换时的重复渲染;③ 列表渲染时确保key唯一,避免无效DOM操作。
六、项目扩展方向:深化实战,拓展功能边界
基础功能实现后,可通过以下方向拓展,提升项目复杂度与实战价值:
- 添加商品搜索功能:结合防抖处理(避免频繁输入触发请求),实现关键词搜索;
- 实现商品收藏功能:用Pinia+localStorage持久化收藏状态,刷新页面后不丢失;
- 集成Mock.js:模拟完整的后端接口,实现增删改查全流程,无需依赖真实后端;
- 添加加载状态:在接口请求时显示loading动画,请求失败时显示错误提示,提升用户体验;
- 适配移动端:使用媒体查询或Element Plus的响应式组件,实现移动端适配。
核心总结
本项目的核心是“组件化思想与状态管理的整合应用”:通过拆分公共组件(筛选、卡片、分页)提升代码复用性,用Pinia统一管理全局状态实现组件联动,用Vue Router实现页面跳转。初学者通过此项目可掌握Vue项目的工程化开发流程,理解“数据驱动视图”的核心逻辑,为后续开发复杂电商项目(如购物车、订单管理)奠定基础。