Skip to content

创建基本的路由与实现导航切换(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 时进度条样式不对;代理未重启会请求到本地站点;不要在每个组件里重复写进度条逻辑,统一放到拦截器里。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:原生方法加强(完整可运行 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 吞掉错误。

练习 ​

  1. 将项目中的路由 path 全部统一为小写,并验证首页、登录、注册、搜索四个入口均无 404。
  2. 在 src/router/index.js 中实现 push/replace 增强:重复导航静默处理,其他错误保留输出;连续点击搜索按钮验证控制台无异常。
  3. 采用“TypeNav 局部组件 + 路由 meta”方案,实现首页和搜索页显示导航、登录和注册页隐藏导航,并说明你改了哪些文件。
  4. 完成 axios 二次封装:统一 baseURL、超时、请求/响应拦截、nprogress 关闭和错误提示;分别测试成功接口与一个 404 接口。
  5. 检查代理配置:说明请求从浏览器到 devServer 再到目标服务器的路径拼接过程,并修复一次 /api/api 重复问题。

原视频对照 ​

#视频时长
1将加强方法优化07:41
2将TypeNav设置为全局组件08:50
3将TypeNav由路由控制显示与隐藏11:41
4调用首页三级分类接口12:35
5使用nprogress产生接口进度06:02
6axios二次封装13:14

小结 ​

  • Vue2 项目必须优先确认 vue-router 主版本,版本错误会表现为“配置都对但就是跑不起来”。
  • 路由 path、router-link、编程式导航地址要保持同一套命名规范,大小写和尾斜杠都算契约。
  • push/replace 的重复导航异常应在路由入口集中处理,不要在每个按钮里散落 catch。
  • TypeNav 是否全局注册不是重点,重点是显示范围要有唯一控制源;推荐用路由 meta 表达页面级显隐。
  • axios 二次封装要统一“请求前、响应后、失败后”三条路径,尤其不能漏掉进度条关闭和错误返回。
  • 代理配置修改后必须重启 devServer;排查接口问题时先看 Network 里的真实请求 URL。
  • 拦截器返回的数据结构一旦确定,组件层就要遵守约定,避免二次取 .data 这类低级排错成本。