主题
创建基本的路由与实现导航切换(Lesson14 2/3)
本页对应 Lesson14,共 6 段视频,约 1 小时 0 分。
学习目标
- 能封装统一的加强方法并抽离到工具模块,完成对 Vue Router
push/replace的加强,并验证页面跳转正常。 - 能将 TypeNav 抽取为组件,分别实现全局注册与局部注册两种方案,并说明两种方案的适用场景。
- 能通过路由
meta配置控制 TypeNav 的显示与隐藏,并验证首页、搜索页显示,注册页等隐藏。 - 能配置开发代理并调用首页三级分类接口,在 TypeNav 组件中获取并渲染分类数据。
- 能安装并使用 nprogress,在请求发起时开启进度条,在成功或失败回调中关闭进度条并验证效果。
- 能完成 axios 二次封装,创建包含
baseURL、timeout、请求拦截和响应拦截的请求实例,并通过统一入口导出和调用接口。
知识点
先列路由表:把页面路径想清楚
为什么需要:这个项目是单页应用,首页、登录、注册、搜索并不是多个 HTML 页面,而是同一个页面里切换不同组件。若不先把“路径 → 组件”的对应关系列清楚,后面写 router-view 和跳转时就会到处临时判断,结构容易乱。
怎么想:先按笔记把最基础的映射定下来:首页 /、登录 /login、注册 /register、搜索 /search。这一步只确定“访问哪个地址显示哪个页面”,暂不处理权限、导航显示隐藏等后续问题。
怎么做:保留本节笔记中的映射关系:
首页:------> /
登陆: --------> /login
注册:---------->/register
搜索:------------>/search
易错点:路径要统一大小写。笔记后面的路由配置里注册写成了 /Register,而上面映射写的是 /register,实际项目要保证两处一致,否则访问 /register 时匹配不到。另外“登录”不要写成“登陆”。
安装 vue-router:Vue2 对应 vue-router 3
为什么需要:Vue 本身只负责组件和视图,路由能力要由 vue-router 提供。项目使用 Vue2,因此要安装与之匹配的 vue-router@3,不能直接装最新版。
怎么想:先确认项目依赖版本,再安装。安装后要通过 Vue.use(VueRouter) 注册插件,否则 <router-view/>、$router 等能力不会生效。
怎么做:
shell
cnpm install vue-router@3易错点:忘记 Vue.use(VueRouter);或 Vue2 项目误装 vue-router4,导致 API 和运行行为不匹配。安装后建议重启开发服务再验证。
配置 src/router/index.js:创建路由实例并导出
为什么需要:把路由表集中在一个文件里,入口文件只负责“注入路由”,页面组件只负责“展示内容”,职责更清楚。
怎么想:先引入 Vue、VueRouter 和四个页面组件,再定义 routes 数组,最后 new VueRouter({ mode:"history", routes }) 并 export default router。history 模式地址更干净,但需要服务端配合。
怎么做:
js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
const routes = [
{
path:"/",
component:Home
},
{
path:"/login",
component:Login
},
{
path:"/Register",
component:Register
},
{
path:"/search",
component:Search
}
];
const router = new VueRouter({
mode:"history",
routes
});
export default router;易错点:path:"/Register" 与预期 /register 不一致;component 引入路径写错;忘记导出 router;mode:"history" 下直接刷新子路径可能 404,本地开发要确认 devServer 的 history fallback,线上要配置服务端回退。
准备页面组件:先让每个路由有可显示的组件
为什么需要:路由配置只是“指路”,真正显示的内容还在页面组件里。先做最简页面,可以快速验证路由是否打通。
怎么想:在 src/pages 下分别建 Home、Login、Register、Search,每个组件只写标题和背景色,先不堆业务逻辑。
怎么做:笔记中 Home 略,其余保留如下。
src/pages/Register/index.vue:
vue
<template>
<div>
<h3>注册界面</h3>
</div>
</template>
<script>
export default {
name: "Register"
}
</script>
<style scoped>
h3{
padding:50px;
background: green;
}
</style>src/pages/Login/index.vue:
vue
<template>
<div>
<h3>登陆界面</h3>
</div>
</template>
<script>
export default {
name: "Login"
}
</script>
<style scoped>
h3{
padding:50px;
background: skyblue;
}
</style>src/pages/Search/index.vue:
vue
<template>
<div>
<h3>搜索界面</h3>
</div>
</template>
<script>
export default {
name: "Search"
}
</script>
<style scoped>
h3{
padding:50px;
background: yellow;
}
</style>易错点:组件 name 与文件名不必强一致,但引入路径必须对;scoped 样式只影响当前组件;模板里最好保留根 div。
App.vue 放路由出口:router-view 决定页面显示在哪
为什么需要:App.vue 是项目最外层,头部和底部固定,中间内容要根据路由切换。<router-view/> 就是路由组件的渲染出口。
怎么想:把 Header、<router-view/>、Footer 按上下结构放好。路由匹配到哪个组件,就在 router-view 位置渲染哪个组件。
怎么做:
vue
<template>
<!-- 项目的最外层 -->
<div>
<!-- 头部 -->
<Header></Header>
<router-view/>
<!-- 底部 -->
<Footer></Footer>
</div>
</template>
<script>
import Home from "@/pages/Home";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default {
name: "App",
components: {Footer, Header, Home}
}
</script>
<style lang="less" scoped>
</style>易错点:<router-view/> 写错或放在 Header 外面会导致页面位置不对;Header、Footer 未注册却使用会报错;Home 在当前模板未使用但被引入,虽不影响运行,但属于冗余,可后续清理。
入口注入 router:让整个 Vue 应用认识路由
为什么需要:router/index.js 只是创建了路由实例,必须挂到 Vue 根实例上,App.vue 里的 router-view 才能工作。
怎么想:在 src/index.js 引入 router,传入 new Vue({ el:"#app", router, render:h=>h(App) })。
怎么做:
js
import Vue from "vue";
import App from "@/App";
import router from "@/router";
import "@/assets/css/reset.css";
new Vue({
el:"#app",
router,
render:h=>h(App)
})易错点:忘记写 router 这一项;import router from "@/router" 要能解析到 src/router/index.js;render:h=>h(App) 不要漏。转写稿中通过依次点击搜索、登录、注册并查看控制台无异常来验证路由切换,这个调试思路可以用:先看地址是否变化,再看 router-view 内容是否切换,最后看控制台有无报错。
本节目标与整体思路
本节在 src/components/Header/index.vue 中完成头部导航切换:登录、免费注册、Logo 回首页用声明式导航,搜索按钮用编程式导航。Vue Router 的核心是“改地址但不刷新页面”,所以链接用 router-link,按钮事件用 $router.push。转写稿里还提到把 push/replace 的增强逻辑抽成函数,避免重复写“先备份再重写”。
声明式导航:登录、注册、Logo 回首页
为什么需要:登录、注册、Logo 都是“点一下去某个路由”的入口,用声明式导航最直观,模板里写清 to 即可。
怎么想:router-link 会渲染成可点击链接,to="/login"、to="/register"、to="/" 分别对应登录、注册、首页;Logo 图片保留在链接内部。
怎么做:保留本节代码,图片 alt 改为中文“尚品汇Logo”:
vue
<template>
<!-- 头部 -->
<header class="header">
<!-- 头部的第一行 -->
<div class="top">
<div class="container">
<div class="loginList">
<p>尚品汇欢迎您!</p>
<p>
<span>请</span>
<!--1-->
<router-link to="/login">登录</router-link>
<!--2-->
<router-link to="/register" class="register">免费注册</router-link>
</p>
</div>
<div class="typeList">
<a>我的订单</a>
<a>我的购物车</a>
<a>我的尚品汇</a>
<a>尚品汇会员</a>
<a>企业采购</a>
<a>关注尚品汇</a>
<a>合作招商</a>
<a>商家后台</a>
</div>
</div>
</div>
<!--头部第二行 搜索区域-->
<div class="bottom">
<h1 class="logoArea">
<!--4-->
<router-link to="/" class="logo" title="尚品汇">
<img src="./images/logo.png" alt="尚品汇Logo">
</router-link>
</h1>
<div class="searchArea">
<form action="###" class="searchForm">
<input type="text" id="autocomplete" class="input-error input-xxlarge" />
<!--3-->
<button @click="$router.push('/search')" class="sui-btn btn-xlarge btn-danger" type="button">搜索</button>
</form>
</div>
</div>
</header>
</template>
<script>
export default {
name: "Header"
}
</script>易错点:to 路径要写对;router-link 不要误写成普通 a;Logo 图片的 alt 要写中文;注册链接的 class="register" 保留,别丢样式。
编程式导航:搜索按钮
为什么需要:搜索是按钮点击行为,不是普通链接,适合在事件里用 $router.push('/search')。
怎么想:按钮在 form 内,默认 type 是 submit,可能触发表单提交导致刷新;要显式写 type="button",再绑定点击事件。
怎么做:如上方代码中的 @click="$router.push('/search')"。点击后地址切到 /search,页面不整体刷新。
易错点:$router 是路由实例,$route 是当前路由信息,别混;路径别漏 /;按钮不写 type="button" 容易出现刷新或跳转异常。
用转写稿补优化:增强 push/replace
为什么需要:转写稿指出,多个地方都要“先备份原方法,再重写方法”,重复写很啰嗦。
怎么想:抽一个 enhance 函数,接收路由实例和要增强的方法名,先备份再重写,调用时传 push、replace。
怎么做:转写稿把它抽到 src/utils 一类工具文件,再在路由配置里调用;核心是“先备份、后重写、按方法名取属性”。
易错点:转写稿调试时曾因封装函数漏接参数,控制台报变量未定义;补上参数后,点击搜索、登录、注册均正常。增强逻辑要放在重写函数内部,别把原方法覆盖丢。
调试与验证
启动项目后依次点击搜索、登录、注册,观察地址变化和控制台:转写稿中的验证结果是点击搜索无异常,点击登录、注册也都没有问题。若报未定义,优先检查封装函数是否接收了参数;若按钮导致刷新,检查 type="button"。
与后续导航组件的关系
转写稿后半段还讨论了 TypeNav 设为全局组件、用路由配置控制显示隐藏、调用首页三级分类接口、nprogress 进度条和 axios 二次封装。这些是在 Header 导航切换之后,对商品分类导航的复用与数据请求;本节先把 Header 的声明式/编程式导航切换做稳,后续再按“公共组件、路由控制、请求封装”推进。
问题现象:Header 中的编程式导航
为什么需要:搜索按钮不是 router-link,但点击后也要跳到 /search,所以要在组件方法里做编程式导航。
怎么想:按钮绑定点击事件,在方法中调用 this.$router.push("/search")。
怎么做:保留 Header 组件代码,图片 alt 改为中文。
易错点:方法里的 this 要指向组件实例;不要用 window.location 做单页跳转。
vue
<template>
<!-- 头部 -->
<header class="header">
<!-- 头部的第一行 -->
<div class="top">
<div class="container">
<div class="loginList">
<p>尚品汇欢迎您!</p>
<p>
<span>请</span>
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
<router-link to="/search" class="register">搜索</router-link>
</p>
</div>
<div class="typeList">
<a>我的订单</a>
<a>我的购物车</a>
<a>我的尚品汇</a>
<a>尚品汇会员</a>
<a>企业采购</a>
<a>关注尚品汇</a>
<a>合作招商</a>
<a>商家后台</a>
</div>
</div>
</div>
<!--头部第二行 搜索区域-->
<div class="bottom">
<h1 class="logoArea">
<router-link to="/" class="logo" title="尚品汇">
<img src="./images/logo.png" alt="尚品汇 logo">
</router-link>
</h1>
<div class="searchArea">
<form action="###" class="searchForm">
<input type="text" id="autocomplete" class="input-error input-xxlarge" />
<button @click="goSearch" class="sui-btn btn-xlarge btn-danger" type="button">搜索</button>
</form>
</div>
</div>
</header>
</template>
<script>
export default {
name: "Header",
methods:{
goSearch(){
this.$router.push("/search");
}
}
}
</script>原生方法增强:先备份再重写
为什么需要:理解“增强”的通用套路,才能看懂后面为什么要改 VueRouter.prototype.push。
怎么想:先保存原生方法,再重写,在重写方法里调用备份。
怎么做:用数组 push 做演示。
易错点:改原生原型只用于理解,生产代码不要随意改 Array.prototype。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
<script>
// 对原生方法加强思路:
// 1- 将原生的方法进行一个备份
// 2- 重写原生方法,在重写方法当中调用备份。
// 常识:
// 1- push是原生程序:native code
// 2- push来自于Array.prototype.push
const arr = [1,2,3,4,5];
// native code
// console.log(arr.push);
// arr.push(8);
// console.log(arr);// [1,2,3,4,5,9]
// push来自于Array.prototype.push
// console.log(Array.prototype.push=== arr.push);// true
// 更改push
// 1- 将原生的方法进行一个备份
const nativePush = Array.prototype.push;
// 2- 重写原生方法,在重写方法当中调用备份。
Array.prototype.push = function(value){
value+=1;
nativePush.call(this,value);
}
arr.push(10);
console.log(arr);// [1,2,3,4,5,11]
</script>
</html>在 Vue Router 原型上增强 push/replace
为什么需要:重复点击同一路由时,push 可能返回 rejected promise,控制台出现 NavigationDuplicated 报错。
怎么想:同样先备份 VueRouter.prototype.push/replace,再重写,第三个参数传空函数吞掉重复导航错误。
怎么做:在 src/router/index.js 中增强。
易错点:必须用 call(this, ...) 保持路由实例上下文;不能丢掉 location 和 onComplete。
js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
// 实现加强(未优化)
// 1- 先将要加强的方法备份
const nativePush = VueRouter.prototype.push;
const nativeReplace = VueRouter.prototype.replace;
// 2- 重写方法,在重写的方法中调用备份方法
VueRouter.prototype.push = function(location, onComplete, onAbort){
return nativePush.call(this,location, onComplete, ()=>{})
}
VueRouter.prototype.replace = function(location, onComplete, onAbort){
return nativeReplace.call(this,location, onComplete, ()=>{})
}
// replace
const routes = [
{
path:"/",
component:Home
},
{
path:"/login",
component:Login
},
{
path:"/Register",
component:Register
},
{
path:"/search",
component:Search
}
];
const router = new VueRouter({
mode:"history",
routes
});
export default router;用 enhance 函数消除重复代码
为什么需要:push 和 replace 的增强逻辑几乎一样,重复写不便于维护。
怎么想:把“属性名”作为参数传入,公共逻辑只写一次。
怎么做:定义 enhance(props),内部备份并重写。
易错点:enhance 必须接收参数;转写稿调试时就出现过 props is not defined,补上参数后测试通过。
js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
// 优化一:定义一个方法enhance
function enhance(props){
const nativeFn = VueRouter.prototype[props];
VueRouter.prototype[props] = function(location, onComplete, onAbort){
return nativeFn.call(this,location, onComplete, ()=>{})
}
}
enhance("push");
enhance("replace");
const routes = [
{
path:"/",
component:Home
},
{
path:"/login",
component:Login
},
{
path:"/Register",
component:Register
},
{
path:"/search",
component:Search
}
];
const router = new VueRouter({
mode:"history",
routes
});
export default router;抽离到 utils/enhance.js
为什么需要:路由配置文件应专注路由表,增强逻辑抽离后更清晰。
怎么想:把增强函数放到 src/utils/enhance.js,在 router/index.js 引入并调用。
怎么做:导出 enVueRouter,分别增强 push、replace。
易错点:抽离后要记得 import;调用时传 "push"、"replace";转写稿中测试点击搜索、登录、注册均无异常。
js
import VueRouter from "vue-router";
// 对路由的加强
export const enVueRouter = function(props){
const nativeFn = VueRouter.prototype[props];
VueRouter.prototype[props] = function(location, onComplete, onAbort){
return nativeFn.call(this,location, onComplete, ()=>{})
}
}js
import {enVueRouter} from "@/utils/enhance";
// 优化二
enVueRouter("push");
enVueRouter("replace");调试过程与易错点
转写稿中的调试顺序是:先验证点击搜索、登录、注册是否正常;再做 enhance 优化;抽离到 utils/enhance.js 时出现 props is not defined,原因是函数没有接收参数,补上后重新测试通过。易错点集中在:备份方法不能用箭头函数导致 this 丢失;重写时要 nativeFn.call(this, ...);第三个参数传空函数用于忽略重复导航错误;抽离后不要忘记引入和调用。
为什么要把 TypeNav 提成全局组件
首页顶部有“全部商品分类”导航,搜索页同样需要这块导航。如果每个页面都复制一份 type-nav 结构,后续改分类、改样式就要改多处,容易漏改。转写稿里的思路是:先确认首页和搜索页都要用,再把导航从 Home 中剪切出来,做成 components/TypeNav 组件,最后在入口文件注册为全局组件。这样 Home、Search 的模板里直接写 <TypeNav></TypeNav> 即可。
易错点:不要因为“全局”方便,就把 Header、Footer 也全局注册。转写稿明确说它们只在 App 中用,没必要全局注册;全局组件越多,依赖越不清晰。
组件拆分:把导航结构搬进 TypeNav
在 src/components/TypeNav/index.vue 中保留原 type-nav 外层结构,模板内部商品分类项较多,下面保留关键骨架(完整分类项按笔记原样放入):
vue
<template>
<div class="type-nav">
<div class="container">
<div class="nav-left">
<h2 class="all">全部商品分类</h2>
<div class="sort">
<div class="all-sort-list2">
<div class="item">
<h3><a>图书、音像、数字商品</a></h3>
<div class="item-list clearfix">
<!-- 二级分类结构按笔记原样保留 -->
</div>
</div>
<!-- 其余商品分类项按笔记原样保留 -->
</div>
</div>
</div>
<nav class="nav">
<a>服装城</a><a>美妆馆</a><a>尚品汇超市</a><a>全球购</a>
<a>闪购</a><a>团购</a><a>有趣</a><a>秒杀</a>
</nav>
</div>
</div>
</template>
<script>
export default {
name: "TypeNav"
}
</script>这里的关键不是重写结构,而是把 Home 里原有导航整块剪切过来,避免样式类名变化。
全局注册:在 src/index.js 挂到 Vue 上
为什么要在入口注册:全局组件只注册一次,所有页面都能用。怎么做:先 import,再 Vue.component,且必须写在 new Vue 之前。
js
import Vue from "vue";
import App from "@/App";
import router from "@/router";
import TypeNav from "@/components/TypeNav";
import "@/assets/css/reset.css";
Vue.component("TypeNav", TypeNav);
new Vue({
el: "#app",
router,
render: h => h(App)
})易错点:@/components/TypeNav 默认会找目录下的 index.js;注册名 TypeNav 要和模板标签一致;全局注册后,Home/Search 里不要再局部注册同名组件。
在 Home 和 Search 中使用
Home 中直接使用;笔记里的 <tempalte> 是笔误,应写成 <template>:
vue
<template>
<TypeNav></TypeNav>
</template>Search 中同样放到最上面:
vue
<template>
<div>
<TypeNav></TypeNav>
<h3>搜索界面</h3>
</div>
</template>
<script>
export default {
name: "Search"
}
</script>
<style scoped>
h3 {
padding: 50px;
background: yellow;
}
</style>调试过程与另一种思路
转写稿中的调试现象是:把导航从 Home 剪走后,首页导航消失;在 src/index.js 注册全局组件,并在 Home 模板加上 <TypeNav> 后,首页恢复;再到 Search 模板加上 <TypeNav>,搜索页也出现。控制台无异常,点击搜索、登录、注册可验证路由切换正常。
转写稿还提到另一种方案:不全局注册,把 TypeNav 放进 Header,用路由 meta 标记哪些页面显示,再在 Header 里用 v-if 控制。本节笔记采用全局组件方案,先完成复用;搜索页下拉默认隐藏、鼠标移入显示属于后续交互,不是注册问题。易错点是把“全局注册”当成“所有页面都必须显示”,显示隐藏应交给路由或组件逻辑控制。
先明确目标:为什么不让 TypeNav 全局注册
为什么需要:转写稿里先对比了两种方案:一种是把 TypeNav 注册成全局组件;另一种是不注册全局,而是放到 Header 里,用路由配置控制显示。全局组件虽然“哪里都能用”,但也会让登录、注册等不需要导航的页面被动拥有它,后面还要额外判断。Header 和 Footer 只在 App 里使用,没必要全局;TypeNav 也只在首页和搜索页需要,所以本节选择“不全局”。
怎么想:把 TypeNav 变成 Header 的局部子组件,再由当前路由的 meta.isTypeNav 决定 Header 里是否渲染它。这样“哪些页面显示导航”集中在路由表里维护。
怎么做:先改路由,给首页和搜索页加标记:
js
import Vue from "vue";
import VueRouter from "vue-router";
import {enVueRouter} from "@/utils/enhance";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
enVueRouter("push");
enVueRouter("replace");
const routes = [
{
path:"/",
component:Home,
meta:{
// 是否使用导航
isTypeNav:true
}
},
{
path:"/login",
component:Login
},
{
path:"/Register",
component:Register
},
{
path:"/search",
component:Search,
meta:{
// 是否使用导航
isTypeNav:true
}
}
];
const router = new VueRouter({
mode:"history",
routes
});
export default router;易错点:meta 要加在真正需要导航的路由上;登录、注册不加,所以 $route.meta.isTypeNav 为 undefined,v-if 不渲染。字段名 isTypeNav 要和 Header 中判断保持一致。
把 TypeNav 迁移到 Header 目录并局部引入
为什么需要:既然不全局注册,就要把 TypeNav 放到使用它的父组件附近。转写稿中把 TypeNav 从 components 根目录剪切到 components/Header 下,再在 Header 中局部注册。
怎么想:Header 负责页面头部,TypeNav 是头部里的导航块;由 Header 引入 TypeNav,比全局注册关系更清楚。
怎么做:将 TypeNav 组件放置到 src->components->Header 目录中,然后改 Header:
js
<template>
<div>
...
<TypeNav v-if="$route.meta.isTypeNav"></TypeNav>
</div>
</template>
<script>
import TypeNav from "@/components/Header/TypeNav";
export default {
name: "Header",
components: {TypeNav},
methods:{
goSearch(){
this.$router.replace("/search");
}
}
}
</script>易错点:import 路径要指向 Header/TypeNav;局部组件必须在 components 中注册;v-if 判断的是 $route.meta.isTypeNav,不是 $route.meta 本身。转写调试时就是通过首页、搜索、注册三个入口验证:首页和搜索有导航,注册没有。
清理全局注册和页面内重复使用
为什么需要:如果还保留 Vue.component("TypeNav", TypeNav),TypeNav 仍然是全局组件,容易让人误以为“没生效”;同时 Home 和 Search 里若还写 <TypeNav></TypeNav>,会和 Header 里的导航重复渲染。
怎么想:全局注册移除后,只保留 Header 内部这一处使用;Home 和 Search 不再单独写 TypeNav。
怎么做:在 src/index.js 中移除全局组件定义:
js
import Vue from "vue";
import App from "@/App";
import router from "@/router";
// import "TypeNav" from "@/components/TypeNav"
import "@/assets/css/reset.css";
// Vue.component("TypeNav",TypeNav)
new Vue({
el:"#app",
router,
render:h=>h(App)
})然后从 src->pages->Home.vue 以及 Search.vue 中的 <TypeNav></TypeNav> 移除。
易错点:只删 Vue.component 但没删 import 注释不影响运行,但容易混淆;Home/Search 里忘记删会重复出现导航。转写稿中反复强调“我是不是把它删掉”,就是为了保证只剩 Header 一处。
顺手优化路由增强方法的调用
为什么需要:转写稿开头提到,push 和 replace 的增强逻辑很像,都是先备份再重写,重复写两遍累赘。
怎么想:抽出 enVueRouter,传入要增强的方法名。转写调试时曾出现“pro is not defined”,原因是函数参数没有正确接收,补上参数后首页、搜索、登录、注册切换正常。
怎么做:保留 router 中的 import {enVueRouter} from "@/utils/enhance"; 以及两次调用即可。易错点:增强函数要接收方法名;utils/enhance 要正确导出;调用两次分别传 "push"、"replace"。
调试与验收
启动项目后按转写稿路径验收:打开首页,导航存在;点击搜索,导航存在;点击注册,导航不出现;Home 和 Search 内不再单独写 TypeNav;控制台无异常。若搜索页没有导航,优先检查该路由是否写了 meta.isTypeNav:true;若注册页也有导航,检查是否还残留全局注册或 Header 判断写错。
本节目标与整体思路
这一节要完成的事情很明确:在首页的 TypeNav 商品分类导航组件中,调用“首页三级分类”接口,把商品分类数据取回来,并先在控制台打印确认。
为什么需要先做这一步?因为后面的导航渲染要依赖真实接口数据,不能先写死。怎么想?先准备请求工具 axios,再解决浏览器跨域/开发环境代理问题,最后在组件挂载时发 GET 请求。怎么做?按“安装依赖 → 配置代理并重启 → 组件内请求 → 控制台验证”的顺序推进。易错点是:包名写错、代理配置后不重启、请求地址与代理前缀不匹配、拿到响应后不知道取 data。
安装 axios
为什么需要:项目里要发 HTTP 请求,浏览器原生 fetch 也能用,但当前项目约定使用 axios,后续封装也更方便。怎么想:先在项目根目录安装依赖,安装完成后才能在组件里 import axios from "axios"。怎么做:在终端进入项目根目录执行安装命令。笔记中写的是:
shell
cnpm install axio这里要特别注意:笔记里的 axio 是笔误,实际包名是 axios,正确命令应为 cnpm install axios。保留笔记代码是为了对照,但真正执行时不要照着 axio 敲。易错点:包名拼写错误会导致安装失败或导入失败;安装依赖要在项目根目录执行,不要在任意目录执行。
配置代理服务器并重启项目
为什么需要:接口完整地址是 http://sph-h5-api.atguigu.cn/api/product/getBaseCategoryList,而项目开发服务器有自己的域名和端口。直接请求完整地址可能遇到跨域限制;同时项目里所有接口都以 /api 开头,适合用代理统一转发。怎么想:在 vue.config.js 的 devServer.proxy 中,把以 /api 开头的请求代理到目标服务器 http://sph-h5-api.atguigu.cn,并开启 changeOrigin: true。怎么做:修改 vue.config.js,笔记代码如下:
js
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 忽略lint检查
lintOnSave:false,
// publicPath:"./",// 更改BASE_URL
devServer:{
open:true,
host:"zhangpeiyue.com",
port:80,
// 1
proxy:{
"/api":{
target:"http://sph-h5-api.atguigu.cn",
changeOrigin:true
}
}
},
pages: {
index: {
// page 的入口
entry: 'src/index.js',
// 设置网站标题
title:"尚品汇"
}
}
})配置完成后必须重启项目,代理才会生效。转写稿里特别强调:不是重启电脑,是重启开发服务器。易错点:改完 vue.config.js 不重启,浏览器里看到的仍是旧配置;请求路径不以 /api 开头,代理规则匹配不到;把 target 写成完整接口路径而不是服务器基础地址。
在 TypeNav 组件中调用接口
为什么需要:数据要在导航组件里使用,所以请求逻辑先放在 src/components/Header/TypeNav/index.vue 中验证。怎么想:接口文档说明请求方式是 GET,地址为 /api/product/getBaseCategoryList,无参数;组件挂载时发请求最合适,因此放在 mounted 中。怎么做:引入 axios,调用 axios.get,在 then 中解构 { data } 并打印。笔记代码如下:
vue
<script>
import axios from "axios";
export default {
name: "TypeNav",
mounted(){
// 请求方式:get
// 请求地址:http://sph-h5-api.atguigu.cn/api/product/getBaseCategoryList
axios.get("/api/product/getBaseCategoryList").then(({data})=>{
console.log(data)
})
}
}
</script>这里请求写 /api/product/getBaseCategoryList,开发服务器会按代理规则转发到目标服务器。易错点:把请求地址写成完整域名,导致绕过代理或产生跨域;忘记 import axios;then 里直接打印整个响应而不知道业务数据在 data 中;接口无参数却额外拼接参数。
调试与验证
为什么需要:接口调用不是写完就算,必须确认请求是否发出、代理是否生效、响应是否成功。怎么想:打开浏览器控制台和 Network 面板,刷新页面,观察是否出现 /api/product/getBaseCategoryList 请求,并查看响应数据。怎么做:重启项目后进入首页,在控制台查看打印结果;如果请求成功但页面没变化,是正常的,因为本节只要求拿到数据并打印。转写稿中还提到可以忽略启动时的 lint 提示,对应 lintOnSave:false。易错点:只看控制台不看 Network,无法判断是请求失败还是打印逻辑没执行;代理未重启时请求可能落到本地站点;看到跨域报错要先检查代理前缀、target 和重启状态。完成以上步骤,本节“调用首页三级分类接口”就完成了。
为什么需要接口进度条
接口请求有等待时间。如果页面点击后没有任何反馈,用户会以为“没点上”或“页面卡死”。nprogress 的作用是在请求发出时显示顶部进度条,在请求成功或失败后关闭进度条,让用户知道“正在加载”。
怎么想:进度条不是业务数据,而是请求状态的视觉反馈。它应该和请求生命周期绑定:请求前 start,请求结束 done。怎么做:先安装 nprogress,在发请求的组件里引入 JS 和 CSS,在 axios 请求前后调用。易错点:只引入 JS 不引入 CSS,进度条没有样式;只在成功回调里 done,失败时进度条会卡住。
先跑通最小用法
本节笔记的下载命令是:
shell
cnpm install nprogress然后在 src/components/Header/TypeNav/index.vue 中使用。笔记代码如下:
vue
<script>
import axios from "axios";
import nprogress from "nprogress";
import "nprogress/nprogress.css"
export default {
name: "TypeNav",
mounted(){
// 请求方式:get
// 请求地址:http://sph-h5-api.atguigu.cn/api/product/getBaseCategoryList
nprogress.start();// 产生进度条
axios.get("/api/product/getBaseCategoryList1").then(({data})=>{
console.log(data)
nprogress.done();
},err=>{
console.log(err)
nprogress.done();
})
}
}
</script>这段代码的推导过程是:组件挂载时发请求,所以放在 mounted;请求前调用 nprogress.start();请求成功进入第一个回调,打印数据后 nprogress.done();请求失败进入第二个回调,打印错误后也要 nprogress.done()。转写稿中特意把地址写成 getBaseCategoryList1 来制造 404,观察失败分支是否也能关闭进度条。调试时打开浏览器控制台和 Network 面板,能看到请求失败,同时进度条被关闭。
注意:笔记里的 getBaseCategoryList1 是演示失败路径的地址;真实接口按文档是 /api/product/getBaseCategoryList。不要把演示地址当成正式地址。
失败分支为什么也要 done
易错点在这里:很多同学只写成功回调,忘记失败回调。网络错误、404、跨域失败、超时都不会走成功回调,如果失败时不调用 done(),进度条会停在页面上。
怎么想:start 和 done 必须成对出现。怎么做:成功和失败都写 nprogress.done();失败时还可以 console.log(err) 方便调试。转写稿中请求一个不存在的地址,控制台输出异常信息,进度条仍然结束,说明失败分支生效。
用 axios 拦截器统一管理进度条
如果每个接口都手写 start/done,代码会重复。转写稿的思路是:对 axios 做二次封装,在请求拦截器里 nprogress.start(),在响应拦截器成功和失败里都 nprogress.done()。
怎么想:进度条属于“所有请求公共行为”,应该放到请求封装层。怎么做:在 src/request 下创建请求模块,创建 axios.create() 实例,配置 baseURL、timeout;请求拦截器启动进度条;响应拦截器成功时关闭并返回响应,失败时关闭、提示错误并返回 Promise.reject。这样组件里只关心业务数据,不再重复写进度条代码。易错点:不要直接修改全局 axios 实例,创建独立实例便于扩展;失败时返回 Promise.reject,否则调用方会误以为成功;修改 vue.config.js 代理后要重启项目,代理才生效。
易错点清单
nprogress 变量名不要和包名混淆;样式路径写 nprogress/nprogress.css;请求前 start,成功和失败都要 done;接口地址不要照抄演示用的错误地址;代理配置以 /api 开头时,target 指向基础地址并设置 changeOrigin: true,改完重启;进度条只是反馈,不要把业务逻辑塞进进度条回调里。
为什么要对 axios 做二次封装
为什么需要: 如果每个组件都直接写 axios.get,那么进度条、baseURL、超时时间、响应体提取、错误提示都要重复写;以后增加别的服务端接口,也不好扩展。转写稿里明确说:不要直接改 axios,而是创建一个实例,给“尚品汇”相关接口单独用。
怎么想: 在 src/request 下建两个文件:sphRequest.js 负责创建实例、配置拦截器;index.js 负责统一导出。组件只从 @/request 拿 sphRequest,不关心底层配置。
怎么做: 先安装依赖:npm i axios nprogress。然后写 src/request/sphRequest.js:
js
import axios from "axios";
import nprogress from "nprogress";
import "nprogress/nprogress.css";
const sphRequest = axios.create({
baseURL:"/api",
timeout:5000
});
// 请求拦截
sphRequest.interceptors.request.use(config=>{
nprogress.start();// 开启进度条
return config;
});
// 响应拦截
sphRequest.interceptors.response.use(response=>{
nprogress.done();// 结束进度条
return response.data;// 返回响应体
},(err)=>{
nprogress.done();// 结束进度条
alert(err);
return new Promise(()=>{});// 返回一个状态为pending的Promise对象
});
export default sphRequest;易错点: 请求拦截里必须 return config,否则请求配置丢失;成功和失败都要 nprogress.done(),转写稿调试时特别强调失败也要关闭进度条;return response.data 后,组件里拿到的就是响应体,不是完整 response;失败时返回 pending 的 Promise,是为了让后续 .then 不再执行。alert(err) 是开发调试用,生产环境一般不需要直接弹。
统一出口 index.js 的作用
为什么需要: 以后可能还有别的服务接口。把 sphRequest 集中到 src/request/index.js,组件引用路径稳定,也方便继续加别的请求实例。
怎么想: 用命名导出,组件里按需引入 { sphRequest }。
怎么做:
js
import sphRequest from "@/request/sphRequest";
export {
// 调用尚品汇相关接口
sphRequest
}易错点: sphRequest.js 是默认导出,index.js 里用 import sphRequest from ...;index.js 对外是命名导出,组件里要写 import {sphRequest} from "@/request";。不要把默认导出和命名导出混用。
在 TypeNav 中调用首页三级分类接口
为什么需要: 商品分类导航需要真实数据,不能写死。转写稿中先确认接口:请求方式 GET、地址以 /api 开头、无参数,然后到 TypeNav 组件里调用。
怎么想: 组件挂载后请求一次,所以放在 mounted。因为 baseURL 已经是 /api,请求地址只写 /product/getBaseCategoryList,不要重复写 /api。
怎么做: 在 src/components/Header/TypeNav/index.vue 中:
vue
<script>
import {sphRequest} from "@/request";
export default {
name: "TypeNav",
mounted(){
sphRequest.get("/product/getBaseCategoryList").then(body=>{
console.log(body)
})
}
}
</script>易错点: 转写稿里配置了 /api 代理,改完代理要重启项目,不是只刷新页面;如果请求失败,先看 Network 里最终请求地址是不是当前站点下的 /api/...;因为响应拦截已经返回 response.data,所以 body 就是响应体。失败时由于返回 pending Promise,后面的 .then 不会继续执行,调试时可先看弹窗和 Network 状态码。转写稿用 404 地址测试过:成功、失败都会结束进度条,失败会弹出异常信息。
调试顺序与验证
为什么需要: 二次封装后链路变长,先确认请求发出、代理生效、响应返回,再确认组件拿到数据。
怎么想: 按“Network 看请求 → 看响应体 → 看控制台输出 → 测失败分支”的顺序排查。
怎么做: 打开浏览器 Network,刷新页面,确认 /api/product/getBaseCategoryList 请求成功;再在控制台看 body 输出;把地址临时改错,验证失败时 nprogress.done() 是否执行、是否弹出错误。
易错点: 忘记引入 nprogress/nprogress.css 时进度条样式不对;代理未重启会请求到本地站点;不要在每个组件里重复写进度条逻辑,统一放到拦截器里。
课堂画面






代码示例
示例一:原生方法加强(完整可运行 HTML,理解 push/replace 加强的原理)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>原生方法加强</title>
</head>
<body>
<script>
// 对原生方法加强的思路:
// 1- 将原生的方法进行一个备份
// 2- 重写原生方法,在重写方法当中调用备份
const arr = [1, 2, 3, 4, 5];
// 常识:
// 1- push 是原生程序:native code
// 2- push 来自于 Array.prototype.push
// console.log(Array.prototype.push === arr.push); // true
// 1- 备份原生方法
const nativePush = Array.prototype.push;
// 2- 重写原生方法,在重写方法中调用备份
Array.prototype.push = function (value) {
value += 1; // 加强逻辑:入参 +1
nativePush.call(this, value); // this 必须指向真正的调用者(数组实例)
};
arr.push(10);
console.log(arr); // [1, 2, 3, 4, 5, 11]
</script>
</body>
</html>- 关键行
const nativePush = Array.prototype.push;:先保存原始引用,否则一旦重写就再也拿不到原生实现,会无限递归。 - 关键行
nativePush.call(this, value):必须用call把this绑回调用者(这里是arr),直接写nativePush(value)会丢失this而报错。 - 易错点:重写函数不能用箭头函数,箭头函数没有自己的
this,this会指向外层,导致数组方法失效。 - 易错点:
push挂在Array.prototype上而不是数组实例上,所以“加强”写在原型上,所有数组实例都会生效——Vue Router 的push/replace同理,挂在VueRouter.prototype上。
示例二:路由加强抽离 + 路由配置(src/utils/enhance.js 与 src/router/index.js)
js
// src/utils/enhance.js —— 对路由的加强
import VueRouter from "vue-router";
export const enVueRouter = function (props) {
// 1- 备份
const nativeFn = VueRouter.prototype[props];
// 2- 重写,在重写方法中调用备份
VueRouter.prototype[props] = function (location, onComplete, onAbort) {
// 第三个参数(失败回调)传一个空函数,吞掉重复导航的报错
return nativeFn.call(this, location, onComplete, () => {});
};
};js
// src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import { enVueRouter } from "@/utils/enhance";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
// 优化二:抽离到外部之后,这里只负责调用
enVueRouter("push");
enVueRouter("replace");
const routes = [
{
path: "/",
component: Home,
meta: { isTypeNav: true } // 是否使用导航
},
{ path: "/login", component: Login },
{ path: "/register", component: Register },
{
path: "/search",
component: Search,
meta: { isTypeNav: true } // 是否使用导航
}
];
const router = new VueRouter({
mode: "history",
routes
});
export default router;vue
<!-- src/components/Header/index.vue 关键片段:用路由 meta 控制 TypeNav 的显示与隐藏 -->
<template>
<div>
<!-- 通过路由配置来控制 TypeNav 的显示与隐藏 -->
<TypeNav v-if="$route.meta.isTypeNav"></TypeNav>
<!-- 声明式导航 -->
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
<!-- 编程式导航 -->
<button @click="goSearch">搜索</button>
</div>
</template>
<script>
import TypeNav from "@/components/Header/TypeNav";
export default {
name: "Header",
components: { TypeNav },
methods: {
goSearch() {
this.$router.push("/search");
}
}
};
</script>- 关键行
VueRouter.prototype[props] = function (...) {...}:用中括号取属性名,一个函数就能同时加强push和replace,避免复制粘贴两份。 - 关键行
nativeFn.call(this, location, onComplete, () => {}):第三个参数onAbort被替换成空函数,这是解决“重复点击跳转同一路由报 NavigationDuplicated”的核心。 - 易错点:
enVueRouter必须在导航真正发生之前执行(放在Vue.use(VueRouter)之后、组件渲染之前),否则拦截不到。 - 易错点:
enVueRouter一定要import VueRouter from "vue-router",它操作的是构造函数原型,不是某个 router 实例。 - 易错点(笔记中的坑):路由表里曾写成
path: "/Register",而router-link to="/register"是小写,大小写不一致会导致跳转 404,统一成/register。 - 易错点:
v-if="$route.meta.isTypeNav"依赖路由meta,没有配meta的路由$route.meta.isTypeNav为undefined,即不渲染 TypeNav——这正是登录/注册页不显示导航的效果。
示例三:axios 二次封装 + nprogress 进度条(src/request)
js
// src/request/sphRequest.js —— 尚品汇相关请求
import axios from "axios";
import nprogress from "nprogress";
import "nprogress/nprogress.css";
// 创建一个实例,而不是直接改 axios
const sphRequest = axios.create({
baseURL: "/api",
timeout: 5000
});
// 请求拦截:发送请求前开启进度条
sphRequest.interceptors.request.use(config => {
nprogress.start(); // 开启进度条
return config; // 必须返回 config,否则请求发不出去
});
// 响应拦截
sphRequest.interceptors.response.use(
response => {
nprogress.done(); // 结束进度条
return response.data; // 只返回响应体
},
err => {
nprogress.done(); // 失败也要结束进度条
alert(err);
return new Promise(() => {}); // 返回一个状态为 pending 的 Promise
}
);
export default sphRequest;js
// src/request/index.js —— 统一出口,方便后续扩展其他服务
import sphRequest from "@/request/sphRequest";
export {
// 调用尚品汇相关接口
sphRequest
};js
// src/components/Header/TypeNav/index.vue —— 组件中调用首页三级分类接口
import { sphRequest } from "@/request";
export default {
name: "TypeNav",
mounted() {
// 请求方式:get
// 请求地址:http://sph-h5-api.atguigu.cn/api/product/getBaseCategoryList
sphRequest.get("/product/getBaseCategoryList").then(body => {
console.log(body);
});
}
};js
// vue.config.js —— 配套的代理配置(改完必须重启项目)
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false,
devServer: {
open: true,
host: "zhangpeiyue.com",
port: 80,
proxy: {
"/api": {
target: "http://sph-h5-api.atguigu.cn",
changeOrigin: true
}
}
},
pages: {
index: {
entry: "src/index.js",
title: "尚品汇"
}
}
});- 关键行
axios.create({ baseURL: "/api", timeout: 5000 }):创建独立实例而不是直接改全局axios,保留扩展性,后续可再加xxxRequest请求别的服务。 - 关键行
return response.data:拦截器里直接剥掉一层,组件里.then(body => ...)拿到的就是响应体,不用再写.data.data。 - 关键行
return new Promise(() => {}):返回一个永远 pending 的 Promise,让调用方的.then不再执行,同时避免控制台抛出未捕获异常。 - 易错点:请求拦截里漏写
return config,请求会直接挂起,网络面板看不到任何请求。 - 易错点:
import "nprogress/nprogress.css"前面不要加./,加了会去项目目录找;不加才是去node_modules里找。 - 易错点:
baseURL: "/api"必须和vue.config.js里的代理前缀一致;代理配置改完必须重启项目才生效,热更新不会让代理生效。 - 易错点:组件里请求路径写
/product/getBaseCategoryList(不要重复写/api),否则会拼成/api/api/...。
常见错误
- 现象:安装 vue-router 后启动报错或路由不生效。 原因:Vue2 项目装了 vue-router@4,版本不匹配。修法:固定使用
vue-router@3,删除 node_modules 和 lock 文件后重装并锁版本。 - 现象:点击“免费注册”进入空白页或 404。 原因:路由配置写的是
/Register,router-link写的是/register,大小写不一致。修法:统一路由 path 与跳转地址为小写/register,并全局检索一遍。 - 现象:重复点击搜索/登录时控制台出现
NavigationDuplicated。 原因:vue-router@3 的push/replace返回 Promise,重复导航会 reject。修法:在路由入口统一增强push/replace,只忽略重复导航,其他错误仍要抛出或记录。 - 现象:登录/注册页仍显示 TypeNav,或首页反而不显示。 原因:
meta.isTypeNav漏配、拼写不一致,或全局注册与局部注册混用。修法:明确采用“局部组件 + 路由 meta 控制”方案,检查v-if="$route.meta.isTypeNav"与对应路由配置。 - 现象:配置
devServer.proxy后接口仍打到本地,返回 index.html 或 404。 原因:修改代理后未重启 devServer,或baseURL与请求路径重复拼出/api/api。修法:重启项目;统一约定baseURL: "/api"时请求只写/product/...。 - 现象:axios 响应拦截已返回
response.data,组件里再取.data得到 undefined。 原因:拦截器已剥离一层响应体。修法:组件中直接使用返回的 body,或取消拦截器剥离,二者只选一种。 - 现象:nprogress 进度条卡住不结束。 原因:只在成功回调
done(),失败或异常路径漏关。修法:在请求/响应拦截器统一start/done,失败分支也必须关闭,并避免返回永远 pending 的 Promise 吞掉错误。
练习
- 将项目中的路由 path 全部统一为小写,并验证首页、登录、注册、搜索四个入口均无 404。
- 在
src/router/index.js中实现push/replace增强:重复导航静默处理,其他错误保留输出;连续点击搜索按钮验证控制台无异常。 - 采用“TypeNav 局部组件 + 路由 meta”方案,实现首页和搜索页显示导航、登录和注册页隐藏导航,并说明你改了哪些文件。
- 完成 axios 二次封装:统一
baseURL、超时、请求/响应拦截、nprogress 关闭和错误提示;分别测试成功接口与一个 404 接口。 - 检查代理配置:说明请求从浏览器到 devServer 再到目标服务器的路径拼接过程,并修复一次
/api/api重复问题。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 将加强方法优化 | 07:41 |
| 2 | 将TypeNav设置为全局组件 | 08:50 |
| 3 | 将TypeNav由路由控制显示与隐藏 | 11:41 |
| 4 | 调用首页三级分类接口 | 12:35 |
| 5 | 使用nprogress产生接口进度 | 06:02 |
| 6 | axios二次封装 | 13:14 |
小结
- Vue2 项目必须优先确认 vue-router 主版本,版本错误会表现为“配置都对但就是跑不起来”。
- 路由 path、
router-link、编程式导航地址要保持同一套命名规范,大小写和尾斜杠都算契约。 push/replace的重复导航异常应在路由入口集中处理,不要在每个按钮里散落catch。- TypeNav 是否全局注册不是重点,重点是显示范围要有唯一控制源;推荐用路由 meta 表达页面级显隐。
- axios 二次封装要统一“请求前、响应后、失败后”三条路径,尤其不能漏掉进度条关闭和错误返回。
- 代理配置修改后必须重启 devServer;排查接口问题时先看 Network 里的真实请求 URL。
- 拦截器返回的数据结构一旦确定,组件层就要遵守约定,避免二次取
.data这类低级排错成本。