简介:专为刚学完Vue基础的新手设计的实操项目,完整模拟跨境电商网站的典型页面结构,包含首页、热点推荐、卖家中心、B类/D类进口商品页、操作指南、新闻资讯、底部导航等12个独立Vue单文件组件。项目基于vue-router实现命名路由、编程式导航(router.push)、动态路由参数传递(如商品ID)、嵌套路由(如分类下的子页面)等核心功能,路由配置统一集中在router.js,配合基础路由守卫做简单访问控制。使用vue-cli 4.x搭建,已预设@别名指向src目录,样式全部采用内联CSS和语义化类名,不依赖Element UI、Ant Design等第三方UI库,降低学习干扰。压缩包不含node_modules,解压后执行npm install安装依赖,再运行npm run serve即可在本地localhost:8080查看效果。配套README.md详细说明启动步骤、目录作用及常见问题,main.js负责Vue实例挂载,App.vue为根组件,views目录存放页面级组件,components目录存放复用型小组件,适合边看边敲、理解页面切换逻辑与路由数据流转。
1. 项目概述:为什么这个练手项目能真正帮你“踩实”Vue路由?
刚学完 Vue 基础——响应式数据、指令、组件通信、生命周期,脑子里概念是有的,但一打开编辑器想写个带跳转的页面,就卡在“怎么让点击按钮后不刷新页面、只换内容?”“URL变了,组件怎么自动加载?”“点商品列表进详情页,ID怎么传过去?”这些看似简单的问题上。不是不会,是没在真实结构里跑过一遍。我带过几十个前端新人,发现一个共性:路由不是背API就能掌握的,它必须在一个有血有肉的页面骨架里反复拆解、组装、调试,才能形成肌肉记忆。
这个项目就是专为解决这个问题设计的。它不叫“Vue Router 教程”,而是一个可运行、可触摸、有业务逻辑的跨境电商网站雏形——首页、热点推荐、卖家中心、B类/D类进口商品页、操作指南、新闻资讯……一共12个独立页面,每个页面都承担明确角色,彼此之间存在真实的跳转关系:比如首页轮播图点击跳转到热点详情页(带ID参数),分类导航栏点击进入不同进口类目(命名路由+查询参数),卖家中心里又嵌套了“我的订单”“我的商品”两个子页面(嵌套路由)。这不是玩具Demo,而是把电商场景里最常出现的5类路由模式——命名路由、编程式导航、动态参数传递、查询参数、嵌套路由——全部塞进一个连贯的用户动线里。
你不需要懂跨境电商,但你能立刻理解“点击‘D类进口’按钮 → URL变成 /import/d → 页面显示D类商品列表”这个过程背后发生了什么;你也不需要会写复杂样式,因为所有CSS都内联在组件里,用的是 header, nav-list, product-card 这类直白类名,没有 .el-button--primary 这种抽象封装,你看一眼HTML就知道样式在哪改。vue-cli 4.x 搭建、@/ 别名预设、router.js 集中配置、基础路由守卫(比如未登录不能进卖家中心)——这些都不是为了炫技,而是还原一个真实小项目的最小必要配置。压缩包里没有 node_modules?对,就是要你亲手敲 npm install,感受依赖安装的过程;npm run serve 启动后看到 localhost:8080 上12个页面丝滑切换,那种“原来如此”的顿悟感,是看十遍文档都换不来的。它适合谁?适合那个刚写完 v-for 渲染列表、想马上试试“点进去看详情”的你;适合那个被嵌套路由文档绕晕、需要一个具体例子来锚定概念的你;更适合那个知道 router.push({ name: 'Product', params: { id: 123 } }) 语法、但不确定 params 和 query 到底该用哪个、什么时候会丢失的你。这不是终点,而是你第一次真正把 Vue 路由“踩”在脚下的起点。
2. 整体架构与设计思路:为什么是这12个页面?路由结构如何分层?
2.1 页面选型逻辑:从电商用户动线反推页面骨架
新手练手最怕“假需求”。这个项目的12个页面不是随便凑数的,而是严格按一个真实跨境电商用户的典型访问路径反向梳理出来的。我们先画一条最朴素的用户动线:
用户打开网站(首页)→ 被首页轮播图吸引(点击跳转热点详情)→ 想了解平台规则(点“操作指南”)→ 看到“进口商品”入口(点进去选B类或D类)→ 在B类列表里选中某商品(跳转详情页)→ 突然想起要卖货(点顶部“卖家中心”)→ 进入卖家中心后想看自己订单(嵌套路由到“我的订单”)→ 顺便看看新闻(底部导航栏切到“新闻资讯”)→ 最后回到首页(底部导航栏切换)。
顺着这条线,我们自然拆出核心页面:
- 首页(Home.vue):所有流量入口,承载轮播图、推荐位、快捷入口;
- 热点推荐页(HotList.vue + HotDetail.vue):展示热点列表,点击进入详情,这是最典型的 params 参数传递场景(/hot/123);
- 操作指南页(Guide.vue):静态内容页,用命名路由 name: 'Guide' 实现语义化跳转,避免硬编码路径;
- 进口商品分类页(ImportCategory.vue):作为父路由,提供B类/D类两个Tab切换,URL为 /import;
- B类/D类商品列表页(ImportBList.vue / ImportDList.vue):作为 ImportCategory.vue 的嵌套路由子页面,URL分别为 /import/b 和 /import/d,复用同一套列表渲染逻辑,仅数据源不同;
- 商品详情页(ProductDetail.vue):从B类或D类列表页跳转而来,接收 id 参数并请求对应商品数据,URL如 /product/456;
- 卖家中心页(SellerCenter.vue):带权限控制的敏感页面,需路由守卫拦截未登录用户;
- 我的订单页(MyOrders.vue) & 我的商品页(MyProducts.vue):嵌套在 SellerCenter.vue 下的两个子视图,通过 <router-view> 渲染,URL为 /seller/orders 和 /seller/products;
- 新闻资讯页(NewsList.vue + NewsDetail.vue):独立信息流,用 query 参数实现分页(/news?page=2&size=10);
- 底部导航栏(BottomNav.vue):全局复用组件,绑定 router-link 实现首页/热点/卖家中心/新闻的快速切换;
- 404页面(NotFound.vue):兜底路由,处理所有未定义路径。
你看,12个页面里,有5个是“容器页”(Home、ImportCategory、SellerCenter、App根组件、BottomNav),7个是“内容页”(HotList、HotDetail、Guide、ImportBList、ImportDList、ProductDetail、NewsList、NewsDetail、MyOrders、MyProducts、NotFound),比例接近1:1,符合真实项目中“布局组件”与“业务组件”的协作关系。这种结构让你练的不是孤立API,而是路由如何组织页面层级、如何划分职责边界、如何让不同模块协同工作。
2.2 路由配置哲学:集中管理、分层清晰、守卫轻量
所有路由配置集中在 src/router.js,这是项目最核心的文件之一。它的设计遵循三个原则:集中、分层、克制。
-
集中:不分散到各个组件里,避免
router.addRoute()动态添加带来的混乱。新手最容易犯的错就是把路由逻辑写进组件的mounted钩子,结果跳转逻辑散落各处,调试时像找线索。这里所有路由都在router.js一次性声明,一眼看清全貌。 -
分层:采用三级嵌套结构:
```javascript
const routes = [
// 一级:顶级页面(Home, HotList, Guide…)
{ path: ‘/’, name: ‘Home’, component: () => import(‘@/views/Home.vue’) },
{ path: ‘/hot’, name: ‘HotList’, component: () => import(‘@/views/HotList.vue’) },
{ path: ‘/hot/:id’, name: ‘HotDetail’, component: () => import(‘@/views/HotDetail.vue’), props: true },// 二级:带嵌套路由的父页面(ImportCategory, SellerCenter)
{
path: ‘/import’,
name: ‘ImportCategory’,
component: () => import(‘@/views/ImportCategory.vue’),
children: [
{ path: ‘’, redirect: ‘b’ }, // 默认子路由
{ path: ‘b’, name: ‘ImportBList’, component: () => import(‘@/views/ImportBList.vue’) },
{ path: ‘d’, name: ‘ImportDList’, component: () => import(‘@/views/ImportDList.vue’) }
]
},
{
path: ‘/seller’,
name: ‘SellerCenter’,
component: () => import(‘@/views/SellerCenter.vue’),
beforeEnter: (to, from, next) => {
// 简单登录守卫:检查 localStorage 是否有 token
const token = localStorage.getItem(‘seller_token’)
token ? next() : next(‘/login’) // 重定向到登录页(虽未实现,但预留位置)
},
children: [
{ path: ‘’, redirect: ‘orders’ },
{ path: ‘orders’, name: ‘MyOrders’, component: () => import(‘@/views/MyOrders.vue’) },
{ path: ‘products’, name: ‘MyProducts’, component: () => import(‘@/views/MyProducts.vue’) }
]
},// 三级:兜底与杂项
{ path: ‘/news’, name: ‘NewsList’, component: () => import(‘@/views/NewsList.vue’) },
{ path: ‘/news/:id’, name: ‘NewsDetail’, component: () => import(‘@/views/NewsDetail.vue’), props: true },
{ path: ‘/:pathMatch(.)’, name: ‘NotFound’, component: () => import(‘@/views/NotFound.vue’) }
]
`` 这种结构让嵌套路由一目了然:/import/b是ImportCategory的子路由,/seller/orders是SellerCenter的子路由。props: true的写法让HotDetail.vue和NewsDetail.vue直接通过props接收id,不用再写this.$route.params.id`,更简洁也更利于组件复用。 -
克制:路由守卫只做最必要的事。
SellerCenter的beforeEnter守卫只检查localStorage里的seller_token,不涉及复杂的鉴权逻辑(那是后端的事),也不做异步验证(新手阶段先保证流程跑通)。NotFound路由用pathMatch(.*)*正则匹配所有未定义路径,比旧版*更精准。这种“够用就好”的设计,避免新手被过度复杂的守卫逻辑吓退,先把主干跑通再说。
2.3 工程配置精简:为什么坚持 vue-cli 4.x 和零 UI 框架?
项目锁定 vue-cli 4.x(而非最新的5.x),是有意为之。4.x 的 webpack 配置更透明,vue.config.js 里只有两行关键配置:
module.exports = {
configureWebpack: {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src') // @别名指向src,写路径再也不用 ../../..
}
}
}
}
没有花哨的 chainWebpack 链式调用,没有 transpileDependencies 的纠结,新手打开 vue.config.js 就能看懂“哦,这就是配别名的地方”。babel.config.js 也极简,只保留 @vue/app 预设,确保 async/await、Object.assign 等语法能正常转换。
坚决不引入 Element UI、Vant 或 Ant Design,原因很实在:UI框架是另一座山,不该和路由这座山一起爬。新手第一次写 router-link,如果还要同时学 .el-button 的用法、.el-menu 的激活逻辑,注意力就被撕成两半。这里的按钮就是 <button class="btn-primary">,菜单就是 <ul class="nav-list"><li><router-link to="/home">首页</router-link></li></ul>,样式直接写在组件 <style> 标签里,比如:
<template>
<div class="product-card">
<img :src="product.image" alt="" class="card-img">
<h3 class="card-title">{{ product.name }}</h3>
<p class="card-price">¥{{ product.price }}</p>
</div>
</template>
<style scoped>
.product-card {
border: 1px solid #eee;
border-radius: 4px;
padding: 12px;
}
.card-img {
width: 100%;
height: 120px;
object-fit: cover;
}
.card-title {
font-size: 16px;
margin: 8px 0 4px;
}
.card-price {
color: #e63946;
font-weight: bold;
}
</style>
所有类名都是语义化的,没有魔法前缀,没有BEM嵌套,改样式时你不需要查文档,直接搜 .card-title 就能找到。这种“裸写CSS”的笨办法,恰恰是新手建立样式与DOM映射关系最快的方式。等你把路由玩熟了,再加UI框架,那时你关注的才是“怎么把 Element 的 el-menu 和 vue-router 的 active-class 结合”,而不是“为什么按钮点了没反应”。
3. 核心功能实现详解:命名路由、编程式导航、参数传递、嵌套路由
3.1 命名路由:告别字符串拼接,拥抱语义化跳转
命名路由是 vue-router 最被低估的特性之一。新手常写 this.$router.push('/hot/123'),看着没问题,但一旦路径调整(比如 /hot 改成 /trending),所有硬编码的地方都要手动改,极易遗漏。命名路由用 name 字符串代替路径字符串,路径变更只需改 router.js 一处。
在 Home.vue 的轮播图区域,代码是这样的:
<template>
<div class="home-banner">
<div
v-for="item in banners"
:key="item.id"
class="banner-item"
@click="$router.push({ name: 'HotDetail', params: { id: item.id } })"
>
<img :src="item.image" :alt="item.title">
<h3>{{ item.title }}</h3>
</div>
</div>
</template>
注意 @click 里的 $router.push({ name: 'HotDetail', params: { id: item.id } })。这里 name: 'HotDetail' 对应 router.js 中定义的 { name: 'HotDetail', path: '/hot/:id', ... }。好处是什么?
- 解耦:Home.vue 完全不知道 HotDetail 的路径是 /hot/:id 还是 /detail/hot/:id,它只认名字;
- 安全:如果 HotDetail 路由被误删,启动时 vue-router 会直接报错 Named Route 'HotDetail' not found,而不是静默失败;
- 可读:{ name: 'HotDetail' } 比 /hot/123 更容易理解意图。
配套的 HotDetail.vue 组件接收参数也很干净:
<template>
<div class="hot-detail">
<h1>{{ detail.title }}</h1>
<p>{{ detail.content }}</p>
</div>
</template>
<script>
export default {
name: 'HotDetail',
props: ['id'], // 因为 router.js 里写了 props: true,所以 id 自动作为 prop 注入
data() {
return {
detail: {}
}
},
created() {
// 模拟 API 请求,实际项目中这里调用 axios
this.fetchDetail(this.id)
},
methods: {
fetchDetail(id) {
// 假数据,实际项目中替换为真实接口
const mockData = {
'123': { id: '123', title: '黑五狂欢开启', content: '全球好物低至3折...' },
'456': { id: '456', title: '保税仓直发攻略', content: '清关时效提升50%...' }
}
this.detail = mockData[id] || { title: '未找到', content: '内容不存在' }
}
}
}
</script>
props: ['id'] 直接声明接收 id,无需 this.$route.params.id。这种写法让组件更纯粹,测试时可以直接传 props 模拟不同ID,不用构造 $route 对象。
提示:命名路由配合
router-link使用更优雅。在BottomNav.vue底部导航里:
vue <router-link to="{ name: 'Home' }" active-class="nav-active">首页</router-link> <router-link to="{ name: 'HotList' }" active-class="nav-active">热点</router-link>
active-class会在当前路由匹配时自动给<a>标签添加nav-active类,实现高亮效果,比手动v-if="$route.name === 'Home'"简洁得多。
3.2 编程式导航:不只是跳转,更是用户行为的精确控制
$router.push() 是编程式导航的核心,但它远不止“跳转”这么简单。在这个项目里,它被用来实现三种典型用户行为:
1. 带参数的详情跳转(params)
如前所述,HotList.vue 列表项点击:
<li v-for="item in hotList" :key="item.id">
<router-link
:to="{ name: 'HotDetail', params: { id: item.id } }"
>
{{ item.title }}
</router-link>
</li>
params 用于标识资源的唯一ID,会体现在URL路径中(/hot/123),特点是:
- 必须在路由 path 中声明占位符(/hot/:id);
- 如果 params 缺失,跳转会失败(NavigationFailureType.redirected 错误);
- 适合做“资源定位”,如文章、商品、用户详情。
2. 带筛选条件的列表跳转(query)
NewsList.vue 的分页和分类筛选:
<div class="news-filters">
<button @click="$router.push({ name: 'NewsList', query: { category: 'policy', page: 1 } })">政策解读</button>
<button @click="$router.push({ name: 'NewsList', query: { category: 'market', page: 1 } })">市场动态</button>
</div>
<!-- 分页控件 -->
<div class="pagination">
<button
@click="$router.push({
name: 'NewsList',
query: { ...$route.query, page: $route.query.page ? parseInt($route.query.page) + 1 : 2 }
})"
>
下一页
</button>
</div>
query 参数附加在URL问号后面(/news?category=policy&page=1),特点是:
- 不需要在 path 中声明,任意添加;
- 可以为空,不影响跳转;
- 适合做“状态筛选”,如分页、搜索关键词、分类标签。
3. 导航守卫中的条件跳转(next())
SellerCenter.vue 的 beforeEnter 守卫:
beforeEnter: (to, from, next) => {
const token = localStorage.getItem('seller_token')
if (token) {
next() // 允许进入
} else {
// next('/login') // 重定向到登录页(项目中暂未实现登录页,此处注释掉)
next(false) // 阻止导航,停留在当前页(模拟未登录提示)
}
}
next() 是守卫的灵魂。next() 允许通行,next(false) 取消导航,next('/login') 重定向。新手常忽略 next() 必须被调用,否则导航会挂起。这里用 next(false) 模拟一个简单的“未登录拦截”,用户点击卖家中心时,页面无反应并弹出提示(SellerCenter.vue 内部有 mounted 钩子检测并提示),比直接重定向更可控。
注意:
params和query不要混用!比如不要写$router.push({ name: 'Product', params: { id: 123 }, query: { ref: 'home' } }),虽然语法允许,但会让URL变成/product/123?ref=home,语义混乱。id是资源标识,必须用params;ref是来源标记,应该用query。项目里所有params都用于资源ID(hot/123,product/456,news/789),所有query都用于状态(page=2,category=tech,sort=price),界限非常清晰。
3.3 嵌套路由:构建多层级页面结构的基石
嵌套路由是 vue-router 区别于简单跳转的关键能力。它让一个页面(父组件)可以包含多个子视图,每个子视图对应一个子路由,共同构成一个完整的功能模块。ImportCategory.vue 和 SellerCenter.vue 是项目里两个典型的嵌套路由应用。
ImportCategory.vue:分类Tab切换
这个页面本身不展示具体内容,只提供B类/D类两个Tab导航,并在下方 <router-view> 渲染对应的子页面:
<template>
<div class="import-category">
<div class="tab-nav">
<router-link
to="{ name: 'ImportBList' }"
active-class="tab-active"
class="tab-item"
>
B类进口商品
</router-link>
<router-link
to="{ name: 'ImportDList' }"
active-class="tab-active"
class="tab-item"
>
D类进口商品
</router-link>
</div>
<!-- 子路由内容在此处渲染 -->
<router-view />
</div>
</template>
router.js 中的配置决定了 ImportBList.vue 和 ImportDList.vue 如何被加载:
{
path: '/import',
name: 'ImportCategory',
component: () => import('@/views/ImportCategory.vue'),
children: [
{ path: '', redirect: 'b' }, // 访问 /import 时默认跳转到 /import/b
{ path: 'b', name: 'ImportBList', component: () => import('@/views/ImportBList.vue') },
{ path: 'd', name: 'ImportDList', component: () => import('@/views/ImportDList.vue') }
]
}
关键点在于:
- 父路由 path: '/import' 的 component 必须包含 <router-view>,否则子路由无法渲染;
- 子路由 path 是相对于父路由的,'b' 表示 /import/b,不是 /b;
- redirect: 'b' 实现了默认子路由,避免访问 /import 时 <router-view> 为空。
SellerCenter.vue:功能模块分区
卖家中心更复杂,它有“我的订单”和“我的商品”两个平行功能区,且都属于卖家中心范畴:
<template>
<div class="seller-center">
<header class="seller-header">
<h2>卖家中心</h2>
<p>欢迎回来,卖家A</p>
</header>
<div class="seller-nav">
<router-link
to="{ name: 'MyOrders' }"
active-class="nav-active"
class="nav-item"
>
我的订单
</router-link>
<router-link
to="{ name: 'MyProducts' }"
active-class="nav-active"
class="nav-item"
>
我的商品
</router-link>
</div>
<!-- 子路由内容在此处渲染 -->
<main class="seller-main">
<router-view />
</main>
</div>
</template>
router.js 中的嵌套配置:
{
path: '/seller',
name: 'SellerCenter',
component: () => import('@/views/SellerCenter.vue'),
children: [
{ path: '', redirect: 'orders' }, // 默认显示我的订单
{ path: 'orders', name: 'MyOrders', component: () => import('@/views/MyOrders.vue') },
{ path: 'products', name: 'MyProducts', component: () => import('@/views/MyProducts.vue') }
]
}
这里 MyOrders.vue 和 MyProducts.vue 是完全独立的组件,它们只关心自己的数据和逻辑,SellerCenter.vue 只负责提供导航和布局容器。这种“父容器+子功能”的模式,正是大型应用拆分路由的标准实践。新手通过这个例子,能直观理解:为什么一个“卖家中心”页面要拆成3个文件?因为职责分离——布局归布局,订单逻辑归订单,商品逻辑归商品。
3.4 路由守卫实战:从登录拦截到页面级权限控制
路由守卫是 vue-router 的安全阀,项目中实现了两种最常用类型:全局前置守卫(router.beforeEach)和路由独享守卫(beforeEnter)。
全局前置守卫:统一处理页面标题
在 src/router.js 底部,添加:
// 全局前置守卫:设置页面标题
router.beforeEach((to, from, next) => {
// 如果路由配置了 meta.title,则设置 document.title
if (to.meta && to.meta.title) {
document.title = to.meta.title + ' - 跨境电商练习站'
} else {
document.title = '跨境电商练习站'
}
next()
})
然后在 router.js 的路由配置中为需要的路由添加 meta 字段:
{ path: '/', name: 'Home', component: () => import('@/views/Home.vue'), meta: { title: '首页' } },
{ path: '/hot', name: 'HotList', component: () => import('@/views/HotList.vue'), meta: { title: '热点推荐' } },
{ path: '/seller', name: 'SellerCenter', component: () => import('@/views/SellerCenter.vue'), meta: { title: '卖家中心' } }
这样,每次路由切换,页面标题都会自动更新,无需在每个组件里写 mounted() { document.title = 'xxx' }。meta 字段是 vue-router 提供的元信息容器,可以放任何你想存的数据,比如 meta: { requiresAuth: true }(需要登录)、meta: { keepAlive: true }(需要缓存)等,是扩展路由能力的钥匙。
路由独享守卫:卖家中心的登录校验
前面已介绍 SellerCenter.vue 的 beforeEnter 守卫,这里补充一个实际调试技巧。新手常遇到“守卫没生效”的问题,原因通常是:
- 守卫函数写在了错误的位置(比如写在 routes 数组外面);
- next() 没有被调用(忘记写 next() 或 next(false));
- localStorage 里根本没有 seller_token,但守卫里没做空值判断。
项目中 SellerCenter.vue 的守卫做了容错:
beforeEnter: (to, from, next) => {
const token = localStorage.getItem('seller_token')
// 显式检查 token 是否存在且非空字符串
if (token && token.trim() !== '') {
next()
} else {
// 弹窗提示(项目中用 alert 模拟,实际项目可用 UI 库)
alert('请先登录卖家账号!')
next(false) // 阻止导航
}
}
token.trim() !== '' 防止 localStorage 里存了空格字符串导致误判。alert 是最原始的提示方式,但它足够清晰地告诉你“守卫触发了”,比静默失败更容易调试。等你熟悉了,再替换成 Element UI 的 Message 或自定义 Toast。
注意:路由守卫的执行顺序是
全局前置 -> 路由独享 -> 组件内守卫。项目中没用到组件内守卫(beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave),因为新手阶段聚焦核心流程,避免过度复杂化。但你可以想象:如果MyOrders.vue需要在离开前确认用户是否保存了修改,就可以在组件里写beforeRouteLeave。
4. 实操过程与完整配置:从零搭建到本地运行的每一步
4.1 环境准备与依赖安装:为什么 npm install 是必经之路?
拿到压缩包后,第一步永远是环境准备。项目基于 vue-cli 4.x,这意味着它依赖 webpack、babel、vue-router 等核心包,这些都不在压缩包里(node_modules 被 .gitignore 排除),必须本地安装。
步骤详解:
1. 解压压缩包:假设解压到 ~/Desktop/vue-ecommerce-router;
2. 打开终端,进入项目根目录:
bash cd ~/Desktop/vue-ecommerce-router
3. 执行 npm install:这是最关键的一步。它会读取 package.json 中的 dependencies 和 devDependencies,下载所有必需的包到 node_modules 文件夹。package.json 关键字段如下:
json { "name": "vue-ecommerce-router", "version": "1.0.0", "description": "Vue Router 跨境电商练手项目", "main": "index.js", "private": true, "scripts": { "serve": "vue-cli-service serve", // 启动开发服务器 "build": "vue-cli-service build", // 构建生产包 "lint": "vue-cli-service lint" // 代码检查 }, "dependencies": { "core-js": "^3.8.3", "vue": "^2.6.14", "vue-router": "^3.5.3" // 路由核心库,版本锁定确保兼容性 }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.15", "@vue/cli-plugin-eslint": "~4.5.15", "@vue/cli-service": "~4.5.15", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", "vue-template-compiler": "^2.6.14" } }
注意 vue-router 版本是 ^3.5.3,这是 Vue 2 生态的稳定版本,与 vue-cli 4.x 完美匹配。如果你强行升级到 vue-router 4.x(Vue 3 专用),项目会直接报错,因为 API 完全不兼容。
- 等待安装完成:
npm install通常需要1-3分钟,取决于网络。你会看到类似added 1234 packages from 567 contributors的提示。如果中途报错(如ENOTFOUND),大概率是网络问题,可尝试npm config set registry https://registry.npm.taobao.org/切换淘宝镜像源。
提示:
npm install不是魔法,它是把package.json里声明的“需求清单”变成本地可执行的代码。新手常跳过这步直接npm run serve,结果报错Cannot find module 'vue-cli-service',就是因为vue-cli-service这个命令行工具还没装进来。记住:npm install是连接代码世界与本地机器的桥梁,没有它,一切命令都无效。
4.2 启动开发服务器:npm run serve 背后的秘密
安装完成后,执行:
npm run serve
这个命令会触发 package.json 中定义的 serve 脚本:vue-cli-service serve。vue-cli-service 是 vue-cli 的核心命令行工具,它做了三件大事:
1. 启动 webpack-dev-server
它会启动一个本地开发服务器,默认监听 http://localhost:8080。这个服务器不是简单的文件服务,而是具备热重载(Hot Reload)能力:当你修改 .vue 文件保存后,浏览器会自动刷新(或局部更新组件),无需手动 F5。这是前端开发效率的基石。
2. 加载 vue-cli 插件
vue-cli-service 会根据 package.json 中的 devDependencies 加载插件,比如:
- @vue/cli-plugin-babel:将 ES6+ 语法转换为浏览器兼容的 ES5;
- @vue/cli-plugin-eslint:在保存时自动检查代码风格;
- @vue/cli-plugin-router:为 vue-router 提供开箱即用的配置(项目中已手动配置,此插件未启用)。
3. 解析 vue.config.js
它会读取项目根目录下的 vue.config.js,应用其中的配置。项目中只有 alias 配置,但实际项目中这里可以配置代理(解决跨域)、CDN 外链、生产环境路径等。
启动成功后,终端会输出:
DONE Compiled successfully in 1234ms 10:23:45 AM
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.1.100:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.
此时,打开浏览器访问 http://localhost:8080,你应该能看到首页。如果打不开,请检查:
- 终端是否显示 Compiled successfully(编译成功);
- 是否有其他程序占用了 8080 端口(可改 vue.config.js 指定端口);
- 浏览器控制台(F12)是否有 Failed to load resource 报错(通常是路径错误)。
4.3 目录结构解析:src 下的 views、components、assets 各司何职?
项目目录结构是理解 Vue 项目组织逻辑的窗口。src 是源码根目录,其下结构清晰分工:
src/views/:页面级组件(Views),对应路由的component。每个文件是一个独立的“页面”,比如Home.vue是首页,HotDetail.vue是热点详情页。它们的特点是:- 通常有
<template>、<script>、<style>三部分; data()返回初始数据,created()/mounted()处理初始化逻辑(如请求数据);- 样式作用域为
scoped,避免污染全局; -
文件名与路由
name一致(HotDetail.vue↔name: 'HotDetail'),便于查找。 -
src/components/:复用型小组件(Components),不直接对应路由,而是被views或其他components引用。项目中目前有: BottomNav.vue:底部导航栏,被App.vue引用;Header.vue:顶部公共头部,被多个页面引用;ProductCard.vue:商品卡片,被ImportBList.vue、ImportDList.vue复用。
这些组件的特点是:- 通常接收
props,通过emit向上抛事件; - 样式更注重复用性,类名如
card-item,nav-link; -
逻辑更纯粹,不处理路由跳转(跳转由父组件控制)。
-
src/assets/:静态资源(Assets),存放图片、字体、样式文件等。项目中assets/images/下有轮播图、商品图等。注意: - 在组件中引用图片要用
require('./assets/images/banner1.jpg'),因为 webpack 需要处理路径; -
assets/styles/common.css是全局样式,定义了body、h1等基础样式,避免每个组件重复写。 -
src/router.js:路由中枢,所有路由配置、守卫、全局前置逻辑都在这里。它是整个应用的“导航地图”。 -
src/main.js:应用入口,创建 Vue 实例并挂载:
```javascript
import Vue from ‘vue’
import App from ‘./App.vue’
import router from ‘./router’ // 引入路由实例
Vue.config.productionTip = false
new Vue({
router, // 注入路由
render: h => h(App)
}).$mount(‘#app’) // 挂载到 index.html 的 #app 元素
```
src/App.vue:根组件,是所有页面的容器。它包含<router-view>,所有views组件都渲染在这里:
vue <template> <div id="app"> <Header /> <router-view /> <!-- 所有页面内容在此处渲染 --> <BottomNav /> </div> </template>
理解这个结构,你就明白了:views 是“谁在干活”,components 是“怎么干活的工具”,router.js 是“派活的老板”,App.vue 是“干活的厂房”,main.js 是“开工仪式”。 新手常混淆 views 和 components,以为所有 .vue 文件都一样。其实 views 是业务入口,components 是乐高积木,它们的职责和复用方式截然不同。
4.4 关键文件逐行解读:router.js 与 main.js 的每一行都值得细究
src/router.js 全文解析(精简版):
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. 安装 VueRouter 插件(必须!否则 $router 无法使用)
Vue.use(VueRouter)
// 2. 定义路由数组
const routes = [
// 首页:path 为 '/',name 为 'Home',component 是懒加载的 Home.vue
{ path: '/', name: 'Home', component: () => import('@/views/Home.vue') },
// 热点列表:命名路由,方便跳转
{ path: '/hot', name: 'HotList', component: () => import('@/views/HotList.vue') },
// 热点详情:带动态参数 :id,props: true 让 id 作为 prop 传入组件
{ path: '/hot/:id', name: 'HotDetail', component: () => import('@/views/HotDetail.vue'), props: true },
// 进口分类:父路由,children 定义子路由
{
path: '/import',
name: 'ImportCategory',
component: () => import('@/views/ImportCategory.vue'),
children: [
{ path: '', redirect: 'b' }, // 默认重定向到 b 类
{ path: 'b', name: 'ImportBList', component: () => import('@/views/ImportBList.vue') },
{ path: 'd', name: 'ImportDList', component: () => import('@/views/ImportDList.vue') }
]
},
// 卖家中心:带路由守卫
{
path: '/seller',
name: 'SellerCenter',
component: () => import('@/views/SellerCenter.vue'),
beforeEnter: (to, from, next) => {
const token = localStorage.getItem('seller_token')
if (token && token.trim() !== '') {
next()
} else {
alert('请先登录卖家账号!')
next(false)
}
},
children: [
{ path: '', redirect: 'orders' },
{ path: 'orders', name: 'MyOrders', component: () => import('@/views/MyOrders.vue') },
{ path: 'products', name: 'MyProducts', component: () => import('@/views/MyProducts.vue') }
]
},
// 新闻列表:支持 query 参数
{ path: '/news', name: 'NewsList', component: () => import('@/views/NewsList.vue') },
// 新闻详情:同样用 props
{ path: '/news/:id', name: 'NewsDetail', component: () => import('@/views/NewsDetail.vue'), props: true },
// 404:兜底路由,匹配所有未定义路径
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') }
]
// 3. 创建 router 实例
const router = new VueRouter({
mode: 'history', // 使用 HTML5 History 模式,URL 无 # 号
base: process.env.BASE_URL, // 通常为 '/',用于部署到子路径
routes // 注入路由数组
})
// 4. 全局前置守卫:设置页面标题
router.beforeEach((to, from, next) => {
if (to.meta && to.meta.title) {
document.title = to.meta.title + ' - 跨境电商练习站'
} else {
document.title = '跨境电商练习站'
}
next()
})
// 5. 导出路由实例,供 main.js 使用
export default router
src/main.js 全文解析:
import Vue from 'vue' // 1. 导入 Vue 构造函数
import App from './App.vue' // 2. 导入根组件 App.vue
import router from './router' // 3. 导入 router 实例(来自 router.js)
// 4. 关闭生产提示(仅开发时显示警告)
Vue.config.productionTip = false
// 5. 创建 Vue 实例
new Vue({
router, // 5.1 注入 router,使所有组件都能访问 this.$router 和 this.$route
render: h => h(App) // 5.2 渲染函数,将 App.vue 作为根组件
}).$mount('#app') // 6. 挂载到 index.html 中 id 为 app 的元素
这两份文件,就是整个 Vue 应用的“心脏”和“骨架”。router.js 定义了“去哪里”,main.js 定义了“怎么去”。新手不必死记硬背,但至少要知道:
- Vue.use(VueRouter) 是启用路由功能的开关;
- new VueRouter({ routes }) 是创建导航系统的动作;
- router.beforeEach 是全局守卫的注册点;
- new Vue({ router }) 是把导航系统注入 Vue 实例的关键;
- $mount('#app') 是最后一步,把 Vue 应用“贴”到 HTML 页面上。
5. 常见问题与排查技巧实录:新手踩过的坑,我都替你试过了
5.1 “页面空白/404”问题:路由配置与组件路径的生死线
现象:启动 npm run serve 后,浏览器打开 http://localhost:8080,页面一片空白,或者显示 Not Found。
排查步骤:
1. 检查终端输出:看是否有 Compiled successfully。如果没有,说明 webpack 编译失败,常见原因是 router.js 语法错误(如少了个逗号、括号不匹配)。仔细看报错行号,修复即可。
2. 检查 router.js 的 path 和 component 路径:这是最高频的错误。比如把 component: () => import('@/views/Home.vue') 写成 component: () => import('./views/Home.vue')(少了 @),webpack 就找不到文件,返回 undefined,导致 <router-view> 渲染空内容。@ 是 vue.config.js 里配置的别名,指向 src 目录,必须用 @/ 开头。
3. 检查 App.vue 是否有 <router-view>:如果 App.vue 里漏写了 <router-view>,所有页面都无法渲染。打开 App.vue,确认模板中有 <router-view />。
4. 检查路由 path 是否冲突:比如定义了 { path: '/hot', ... } 和 { path: '/hot/:id', ... },但 /hot/:id 的 path 写成了 /hot/id(少了冒号),那么访问 /hot/123 就会匹配不到,落到 NotFound。
速查表:
| 现象 | 最可能原因 | 解决方案 |
|------|------------|----------|
| 终端报错 Cannot find module '@/views/Home.vue' | @ 别名路径错误或文件不存在 | 检查 vue.config.js 的 alias 配置,确认 Home.vue 在 src/views/ 下 |
| 页面空白,控制台无报错 | App.vue 缺少 <router-view> | 打开 App.vue,添加 <router-view /> |
| 访问 /hot/123 显示 404 | HotDetail 路由 path 写错(如 /hot/id) | 检查 router.js,确保是 /hot/:id,冒号不能少 |
| 点击 router-link 无反应 | router-link 的 to 属性语法错误 | 检查是否写成 to="/hot/123"(字符串)而非 :to="{ name: 'HotDetail', params: { id: 123 } }"(对象) |
5.2 “参数接收不到”问题:props、$route、query 的迷宫
现象:在 HotDetail.vue 里,this.$route.params.id 是 undefined,或者 props 没有接收到 id。
根本原因:对 props 机制和 params/query 的区别理解不清。
解决方案:
- 确认 router.js 中该路由是否设置了 props: true:只有设置了,params 才会作为 props 注入。如果没设,就必须用 this.$route.params.id。
- 确认 params 是否在 path 中声明:{ path: '/hot/:id', ... } 中的 :id 是必须的。如果写成 { path: '/hot', ... },即使 push 时传了 params,$route.params 也是空对象。
- 区分 params 和 query:params 是路径参数,必须匹配 path;query 是查询参数,附加在 URL 后,用 this.$route.query.xxx 获取。比如 $router.push({ name: 'NewsList', query: { page: 2 } }),在 NewsList.vue 里用 this.$route.query.page 获取。
调试技巧: 在组件的 created() 钩子里打印:
created() {
console.log('this.$route:', this.$route)
console.log('this.$route.params:', this.$route.params)
console.log('this.$route.query:', this.$route.query)
console.log('this.id (props):', this.id) // 如果 props: true,这里应该有值
}
看控制台输出,立刻定位是 params 没传过来,还是 props 没配置。
5.3 “嵌套路由不显示”问题:父组件的 <router-view> 是灵魂
现象:访问 /import/b,页面只显示 ImportCategory.vue 的 Tab 导航,下方空白,ImportBList.vue 没渲染。
原因:ImportCategory.vue 组件里缺少 <router-view> 标签。
解决方案: 打开 ImportCategory.vue,确认模板中有:
<template>
<div class="import-category">
<!-- Tab 导航 -->
<div class="tab-nav">
<router-link to="{ name: 'ImportBList' }">B类</router-link>
<router-link to="{ name: 'ImportDList' }">D类</router-link>
</div>
<!-- 关键!子路由内容渲染点 -->
<router-view />
</div>
</template>
<router-view /> 就像一个“插槽”,告诉 Vue:“这里请把匹配到的子路由组件放进来”。没有它,子路由再正确也无处安放。
5.4 “样式不生效”问题:scoped 与全局样式的边界
现象:在 Home.vue 里写了 <style scoped>,但 .home-banner 类名在浏览器开发者工具里看不到样式。
原因:scoped 会为样式添加属性选择器(如 .home-banner[data-v-123abc]),如果 CSS 写错了,或者 HTML 结构不匹配,样式就不会应用。
排查方法:
1. 打开浏览器开发者工具(F12),切换到 Elements 标签,找到 <div class="home-banner"> 元素;
2. 看右侧 Styles 面板,是否列出了 Home.vue 中的 CSS 规则;
3. 如果没列出,检查 <style> 标签是否写了 scoped 属性,以及类名是否拼写正确;
4. 如果列出了但被划掉(strikethrough),说明有更高优先级的样式覆盖了它,比如 assets/styles/common.css 里定义了同名类。
经验心得: 新手初期,建议所有组件样式都用 scoped,避免意外污染。等熟悉了,再在 assets/styles/ 下写全局样式。项目中 common.css 只定义了 body, h1, p 等基础样式,不与组件类名冲突,是安全的。
5.5 “路由守卫不触发”问题:next() 的隐形陷阱
现象:在 SellerCenter.vue 的 beforeEnter 守卫里写了 console.log('guard triggered'),但点击导航时控制台没输出。
原因:守卫函数没有被正确挂载,或者 next() 没有被调用导致导航挂起。
检查清单:
- ✅ 守卫是否写在 router.js 的对应路由对象内(beforeEnter 是路由独享守卫,不是全局);
- ✅ next() 是否在所有分支都被调用(if/else 里都要有 next());
- ✅ localStorage.getItem('seller_token') 返回的是 null 还是字符串?null 会被 if (null) 判为 false,但 if (null && null.trim() !== '') 会短路,不报错。
终极调试法: 在守卫开头加 debugger:
beforeEnter: (to, from, next) => {
debugger // 执行到这里会暂停,可查看 to/from 参数
const token = localStorage.getItem('seller_token')
if (token && token.trim() !== '') {
next()
} else {
alert('请先登录!')
next(false)
}
}
然后点击导航,浏览器会自动断点,你可以一步步看变量值。
最后分享一个小技巧:新手调试路由,永远先看
this.$route。在任何一个组件的created()里写console.log(this.$route),它会打印出当前路由的完整对象,包含path,name,params,query,meta等所有信息。这是最直接、最不会骗你的“真相之眼”。比猜、比查文档、比问人都快。这个项目里,所有页面都值得你打开控制台,敲一行console.log(this.$route),然后点点导航,观察数据变化——这才是路由学习的正道。
我在实际带新人时发现,那些进步最快的学员,不是代码写得最多的,而是控制台打得最勤的。他们不满足于“页面能跳”,而是执着于“为什么能跳”、“数据从哪来”、“参数怎么走”。这个项目,就是为你准备的一块磨刀石。当你把12个页面的每一次跳转、每一个参数、每一处守卫都亲手调试过、理解透,Vue 路由对你来说,就不再是 API 文档里的冰冷文字,而是你指尖下呼之即来的工具。接下来,你可以试着给 NewsList.vue 加个搜索框,用 query 参数实现搜索跳转;或者给 ImportCategory.vue 加个“全部商品”Tab,配置一个新路由;甚至把 localStorage 的 token 换成真实的登录接口。路,就从这12个页面开始,稳稳地向前延伸。
简介:专为刚学完Vue基础的新手设计的实操项目,完整模拟跨境电商网站的典型页面结构,包含首页、热点推荐、卖家中心、B类/D类进口商品页、操作指南、新闻资讯、底部导航等12个独立Vue单文件组件。项目基于vue-router实现命名路由、编程式导航(router.push)、动态路由参数传递(如商品ID)、嵌套路由(如分类下的子页面)等核心功能,路由配置统一集中在router.js,配合基础路由守卫做简单访问控制。使用vue-cli 4.x搭建,已预设@别名指向src目录,样式全部采用内联CSS和语义化类名,不依赖Element UI、Ant Design等第三方UI库,降低学习干扰。压缩包不含node_modules,解压后执行npm install安装依赖,再运行npm run serve即可在本地localhost:8080查看效果。配套README.md详细说明启动步骤、目录作用及常见问题,main.js负责Vue实例挂载,App.vue为根组件,views目录存放页面级组件,components目录存放复用型小组件,适合边看边敲、理解页面切换逻辑与路由数据流转。
274

被折叠的 条评论
为什么被折叠?



