主题
渲染数据与加载中(Lesson8 3/3)
本页对应 Lesson08,共 5 段视频,约 56 分钟。
学习目标
- 能够定义组件数据状态,并使用
v-for与:key将接口返回的列表数据渲染到视图中。 - 能够通过加载状态配合
v-if/v-else实现“加载中”与内容区域的切换,并在请求异常时给出可用的错误提示。 - 能够将 axios 挂载到 Vue 原型或封装为独立模块,配置
baseURL与响应拦截器,并正确说明拦截器返回值对请求结果的影响。 - 能够将静态页面迁移到 Vue 项目,正确引入第三方样式与本地样式,并处理
baseURL导致的资源路径问题。 - 能够使用
ref获取搜索关键词,完成参数校验、接口调用、结果渲染,并覆盖初始、加载中、结果为空、请求异常等状态。 - 能够将搜索区域与结果列表拆分为独立组件,并通过组件传值完成搜索与结果展示的联动。
知识点
先把原示例放进来:从接口数据到列表渲染
为什么需要:拿到接口数据不等于页面会自动显示。Vue 的视图由“数据状态”驱动,必须先把接口返回的数据放进组件状态,再在模板里遍历这个状态。
怎么想:mounted 里请求 GitHub 仓库搜索接口,返回结果在 data.items。模板不能直接写 v-for="item in data.items",因为这里的 data 不是组件实例里的响应式状态;应先在 data() 中声明 items: [],请求成功后赋值 this.items = data.items。
怎么做:保留本节笔记代码如下。
html
<template>
<div>
<h3>App</h3>
<div v-for="item in items" :key="item.id">
<p>{{item.full_name}}</p>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
data(){
return {
items:[]
}
},
async mounted(){
const {data} = await axios.get("https://api.github.com/search/repositories",{
params: {
q: "r",
sort: "stars"
}
});
this.items = data.items;
}
}
</script>
<style scoped>
</style>易错点:v-for 要加 :key,这里用 item.id;字段名是 full_name,不要写成 fullname;接口返回结构是 { data: { items: [...] } },所以解构 { data } 后还要取 data.items。
加载中与异常提示:不要只看数组长度
为什么需要:请求发出到返回之间有空白期,用户会以为页面坏了;请求失败时也不能一直显示“加载中”。
怎么想:用一个 isLoading 表示加载状态,默认 true;请求结束设 false。但失败时如果只写 try 里的 false,catch 分支不会执行到,页面会卡在“正在拼命加载中”。因此要在 finally 或 catch 中关闭 loading,并增加 isError。
怎么做:模板可用 v-if / v-else-if / v-else 分层判断;注意 v-if 和 v-for 不建议同级混用,可外套 template。
html
<h3 v-if="isLoading">正在拼命加载中</h3>
<h3 v-else-if="isError">产生异常,请稍后再试</h3>
<template v-else>
<div v-for="item in items" :key="item.id">
<p>{{ item.full_name }}</p>
</div>
</template>易错点:isLoading 初始值要结合场景设置;异常分支要能覆盖成功和失败;转写稿中频繁刷新接口会触发限制,这属于接口保护,不是“数组长度判断”能解决的问题。
简单封装 axios:baseURL、原型挂载与响应拦截器
为什么需要:每个组件都 import axios、都写完整地址和 data 解构,重复且容易漏。封装后统一配置和响应处理。
怎么想:方案一是在入口把 axios 挂到 Vue.prototype.$http;方案二是设置 axios.defaults.baseURL;方案三是加响应拦截器,让组件直接拿业务数据。转写稿还演示了把请求配置放到 src/request 外部文件。
怎么做:核心是拦截器返回什么,组件就拿到什么。
js
axios.interceptors.response.use(
res => res.data,
err => Promise.reject(err)
)组件里可写:
js
const data = await this.$http.get("/search/repositories", {
params: { q: "r", sort: "stars" }
});
this.items = data.items;易错点:转写稿调试发现“找不到 items”,原因是拦截器返回了 res.data 后,组件又按 { data } 解构,得到 undefined;多个响应拦截器时,后一个拿到的是前一个的返回值,若第一个返回 res.data,第二个再取 res.data.data 就会出错。还要检查 params 拼写,参数写错会出现 422。
静态页面迁移:样式、baseURL 与入口
为什么需要:先把静态页面放进脚手架,才能在其上做搜索交互。
怎么想:把页面结构放入组件,把 Bootstrap 等第三方样式放到 public 或入口引入;注意静态资源路径。转写稿中 baseURL 代表站点根目录 /,link 中建议用根路径,避免单页面应用路由变化后相对路径失效。
怎么做:入口 main.js 正常挂载 App;在 index.html 或入口引入样式;删掉不存在的 source map 警告;可用 Vue.config.productionTip = false 关闭开发提示。
易错点:不要把 baseURL 当成普通字符串随意替换;样式路径写成相对路径可能导致刷新后丢失;第三方样式和组件样式要分清全局与 scoped。
搜索功能:ref 取关键词、状态分层与空结果
为什么需要:搜索页不只有“有数据”一种状态,还有未搜索、加载中、空结果、异常。
怎么想:用 ref 拿输入框值;点击按钮触发 searchUser;先判断关键词长度,为空就提示并 return;请求前设 isSearched=true、isLoading=true,请求结束关闭 loading;结果为空用 items.length === 0 判断。
怎么做:模板按状态显示提示:
html
<h3 v-if="!isSearched">在上方输入关键词并点击搜索</h3>
<h3 v-else-if="isLoading">正在拼命加载中</h3>
<h3 v-else-if="isError">搜索过于频繁,请稍后再试</h3>
<h3 v-else-if="items.length === 0">搜索出来的结果为空</h3>
<template v-else>
<div v-for="item in items" :key="item.id">
<a :href="item.html_url">{{ item.login }}</a>
<img :src="item.avatar_url" alt="用户头像">
</div>
</template>易错点:关键词为空不要发请求;catch 中要设 isError=true,finally 中设 isLoading=false;字段名按接口实际返回写,转写稿里用户搜索用 login、avatar_url、html_url,不要和仓库接口的 full_name 混用。
先理解本节目标:请求没回来时不能只留空白
这一节要解决的是“加载中”和“异常提示”。前面已经能从 GitHub 搜索仓库接口拿到 items,但请求需要时间,页面会先出现一段空白;如果请求失败,用户也只看到空白,会怀疑网络或程序有问题。所以要用响应式状态控制三种界面:加载中、异常、正常列表。
为什么需要:不能直接遍历 data,因为 data 是配置对象,不是响应式数据状态;视图要跟着状态变化重新渲染。
怎么想:先定义 items: [],请求成功后把 data.items 赋给 this.items,模板里再 v-for。
怎么做:在 mounted 中发请求,赋值后 Vue 会自动更新视图。
易错点:不要写 v-for="item in data";key 用唯一标识,如 item.id;渲染字段是 item.full_name。
用 isLoading 管理“正在拼命加载中”
请求发出后、数据回来前,页面需要提示。转写稿里特别强调:不要用 items.length 判断是否加载完,因为接口成功返回空数组时长度也是 0,会把“空结果”误判成“还在加载”。更稳妥的是单独设一个 isLoading。
为什么需要:加载状态和列表数据是两件事,不能用数组长度代替。
怎么想:打开页面就开始请求,所以 isLoading 默认 true;请求成功拿到数据后改为 false。
怎么做:data 中写 isLoading: true;成功回调里写 this.items = data.items; this.isLoading = false;;模板中用 <h3 v-if="isLoading">正在拼命加载中……</h3>。
易错点:v-if 和 v-for 不建议写在同一个元素上;列表部分可以用 <template v-else> 包住,template 不会额外生成 DOM。
用 try/catch 和 isError 处理异常提示
转写稿中演示了频繁刷新 GitHub 接口会报错。异常发生时,如果只在成功分支里把 isLoading 设为 false,页面会一直停在“正在拼命加载中”。因此 catch 里也要结束加载状态,并标记异常。
为什么需要:请求可能失败,用户需要知道“产生异常,请稍后再试”,而不是一直等。
怎么想:isError 默认 false;进入 catch 后先 this.isLoading = false,再 this.isError = true。模板判断顺序是先 isLoading,再 isError,最后才是正常列表。
怎么做:使用 async mounted 加 try/catch,模板中用 v-else-if="isError" 显示异常文案。
易错点:catch 里漏写 isLoading = false 会导致异常时卡在加载中;判断顺序不能乱;v-else-if 必须紧跟在 v-if 后面。转写稿还提到,如果后续封装 axios 拦截器,拦截器要返回响应体,否则业务里可能拿到 undefined。
完整代码:三种状态用 v-if / v-else-if / v-else 串起来
下面保留本节笔记代码。结构是:加载中 <h3>、异常 <h3>、正常列表 <template v-else>。这样请求成功显示列表,失败显示异常,请求中显示加载提示。
vue
<template>
<div>
<h3>App</h3>
<h3 v-if="isLoading">正在拼命加载中……</h3>
<h3 v-else-if="isError">产生异常,请稍后再试……</h3>
<template v-else>
<div v-for="item in items" :key="item.id">
<p>{{item.full_name}}</p>
</div>
</template>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
data(){
return {
items:[],
isLoading:true,
isError:false,// 是否有异常
}
},
async mounted(){
try{
const {data} = await axios.get("https://api.github.com/search/repositories",{
params: {
q: "r",
sort: "stars"
}
});
this.items = data.items;
this.isLoading = false;
}catch (err){
this.isLoading = false;
this.isError= true;
}
}
}
</script>
<style scoped>
</style>易错点再强调:v-else 前不要插入其他同级元素;isLoading 和 isError 在成功、失败分支都要成对处理;列表 key 用 item.id;接口参数 q 和 sort 要放在 params 里。这个“加载中 / 异常 / 正常数据”的三态思路,也可以迁移到后面的搜索案例:再加“是否已搜索”“结果为空”等状态,按顺序判断即可。
1. 为什么要封装 axios
为什么需要: 如果每个组件都 import axios from "axios",再分别写 baseURL、响应拦截器,配置会重复;改接口地址时还要到处改。转写里说,同一模块多次引入虽然会命中缓存,但配置分散仍然难维护,所以要把 axios 的公共配置收拢。
怎么想: 先让 axios 只配置一次,再把配置好的 axios 挂到 Vue.prototype.$axios,组件里统一用 this.$axios 发请求。响应拦截器里 return res.data,组件拿到的就是响应体,不用每次写 res.data。
怎么做: 本节给了三种方案:main.js 里直接配置并挂原型;把配置放到 src/request/index.js 再导出;把挂载逻辑写成插件 gitHub.js,用 Vue.use(gitHub) 安装。
易错点: 不要在多个地方重复给同一个 axios 加响应拦截器。转写调试时就是两个拦截器都执行,第二个拦截器拿到的 res 已经是上一个拦截器返回的 res.data,再写 return res.data 就变成 undefined,组件解构 items 时报错。拦截器的返回值就是下一次/组件拿到的值。
2. 方案一:在 main.js 里直接配置并挂原型
为什么需要: 这是最直观的方案,先理解 axios 配置和 Vue 原型挂载的关系。
怎么想: 在 beforeCreate 里配置 axios.defaults.baseURL,加响应拦截器,然后 Vue.prototype.$axios = axios。这样所有组件都能通过 this.$axios 使用。
怎么做: 对应笔记中 main.js 的注释 1:
js
import Vue from "vue";
import App from "@/App";
import axios from "axios";
// import request from "@/request"
import gitHub from "@/request/gitHub";
new Vue({
render:h=>h(App),
beforeCreate() {
// 1
// axios.defaults.baseURL = "https://api.github.com";
// axios.interceptors.response.use(res=>{
// return res.data;
// })
// Vue.prototype.$axios = axios;
// 2- 将axios配置信息放至外部
// Vue.prototype.$axios = request;
// 3- 安装插件
Vue.use(gitHub);
}
}).$mount("#app");易错点: Vue.prototype.$axios = axios 要放在能执行到的生命周期里;配置和拦截器只写一次。组件里 await this.$axios.get(...) 得到的已经是 res.data,不要再按原始响应写 res.data.items。
3. 方案二:把配置放到 request/index.js
为什么需要: main.js 是入口文件,不应该塞太多请求配置。把 axios 配置放到独立模块,入口只负责挂载。
怎么想: src/request/index.js 引入 axios,设置 baseURL,加响应拦截器,最后 export default axios。main.js 引入 request,挂到原型。
怎么做: 笔记代码:
js
import axios from "axios";
axios.defaults.baseURL = "https://api.github.com";
axios.interceptors.response.use(res=>{
return res.data;
});
export default axios;易错点: 导出的是配置后的 axios 实例/对象,不要再在 main.js 里重复加拦截器。转写中提到项目里可用 axios.create 分实例,但本节只请求一个服务器,所以直接导出 axios;不要把这当成已经写过的代码。
4. 方案三:用插件 gitHub.js 安装
为什么需要: 把“给 Vue 原型挂 $axios”这件事封装成插件,入口只需 Vue.use(gitHub),更像项目里的统一安装方式。
怎么想: 插件导出一个函数,Vue 调用时把 Vue 传进来,在函数里完成 Vue.prototype.$axios = axios。axios 的 baseURL 和拦截器仍在这个模块里配置。
怎么做: 笔记代码:
js
import axios from "axios";
axios.defaults.baseURL = "https://api.github.com";
axios.interceptors.response.use(res=>{
console.log(0,res);
return res.data;
});
export default function(Vue){
Vue.prototype.$axios = axios;
}易错点: Vue.use(gitHub) 时插件函数会被调用;不要同时再在 main.js 里写一遍 Vue.prototype.$axios = axios 或再加同一个响应拦截器。转写里的报错就是“多个拦截器叠加”导致的。
5. App.vue 里加载中、异常和渲染
为什么需要: 请求有等待、成功、失败三种状态,界面不能一直空白,也不能异常时还显示“正在加载”。
怎么想: 用 isLoading 默认 true 表示请求中,isError 默认 false 表示无异常;成功时把响应体里的 items 赋给 data.items,并把 isLoading 置 false;失败时也要把 isLoading 置 false,再把 isError 置 true,否则会一直停在“正在拼命加载中”。
怎么做: 笔记代码:
vue
<template>
<div>
<h3>App</h3>
<h3 v-if="isLoading">正在拼命加载中……</h3>
<h3 v-else-if="isError">产生异常,请稍后再试……</h3>
<template v-else>
<div v-for="item in items" :key="item.id">
<p>{{item.full_name}}</p>
</div>
</template>
</div>
</template>
<script>
export default {
name: "App",
data(){
return {
items:[],
isLoading:true,
isError:false,// 是否有异常
}
},
async mounted(){
try{
const {items} = await this.$axios.get("/search/repositories",{
params: {
q: "r",
sort: "stars"
}
});
// console.log(2222,res);
this.items = items;
this.isLoading = false;
}catch (err){
console.log(1111,err);
this.isLoading = false;
this.isError= true;
}
}
}
</script>
<style scoped>
</style>易错点: 不能直接遍历 data,要先有 items 数据状态,请求成功后再赋值,视图才会重新渲染。v-if / v-else-if / v-else 建议用 template 包住 v-for,避免同层混用导致结构混乱。异常分支里先关加载态,再显示错误态。
课堂画面





代码示例
示例一:渲染数据 + 加载中 / 异常状态(14-2、14-3)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>14-2 / 14-3 渲染数据与加载中</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
#app { width: 600px; margin: 0 auto; font-size: 14px; }
#app p { border-bottom: 1px dashed #ccc; padding: 4px 0; }
</style>
</head>
<body>
<div id="app">
<h3>App</h3>
<h3 v-if="isLoading">正在拼命加载中……</h3>
<h3 v-else-if="isError">产生异常,请稍后再试……</h3>
<template v-else>
<div v-for="item in items" :key="item.id">
<p>{{ item.full_name }}</p>
</div>
</template>
</div>
<script>
// 全局配置 + 响应拦截器:直接返回 res.data
axios.defaults.baseURL = "https://api.github.com";
axios.interceptors.response.use(res => {
return res.data;
});
new Vue({
el: "#app",
data() {
return {
items: [],
isLoading: true, // 默认就在加载
isError: false // 是否有异常
};
},
async mounted() {
try {
const { items } = await axios.get("/search/repositories", {
params: { q: "r", sort: "stars" }
});
this.items = items;
this.isLoading = false;
} catch (err) {
console.log(err);
this.isLoading = false; // 关键:异常时也要关掉 loading
this.isError = true;
}
}
});
</script>
</body>
</html>关键行与易错点
data()里必须先声明items: [],视图才能拿到响应式数据;直接在模板里写data.items或{{ data }}是不行的,data不是数据状态。v-for必须写:key="item.id",用接口返回的唯一标识,不要用索引。v-if / v-else-if / v-else三者必须相邻,中间不能插别的元素;列表用<template v-else>包起来,避免多套一层div。- 最容易出错的地方是:
catch里只写了this.isError = true,忘了把isLoading = false,于是页面一直停在“正在拼命加载中……”,用户根本看不到异常提示。 - 拦截器
return res.data之后,组件里就能直接解构const { items } = await axios.get(...),不用再写res.data.items;但拦截器必须 return,不 return 外层拿到的就是undefined。 - 刷新太频繁时 GitHub 接口会返回 403,正好走到
catch,可以借此验证异常分支。
示例二:封装 axios 的三种方案(14-4)
js
// src/request/index.js —— 方案二:把配置信息放外部
import axios from "axios";
axios.defaults.baseURL = "https://api.github.com";
axios.interceptors.response.use(res => {
return res.data;
});
export default axios;js
// src/request/gitHub.js —— 方案三:导出插件函数,供 Vue.use 使用
import axios from "axios";
axios.defaults.baseURL = "https://api.github.com";
axios.interceptors.response.use(res => {
console.log(0, res);
return res.data;
});
export default function (Vue) {
Vue.prototype.$axios = axios;
}js
// src/main.js
import Vue from "vue";
import App from "@/App";
// import request from "@/request"; // 方案二
import gitHub from "@/request/gitHub"; // 方案三
new Vue({
render: h => h(App),
beforeCreate() {
// 方案一:全部写在 main.js 里
// axios.defaults.baseURL = "https://api.github.com";
// axios.interceptors.response.use(res => res.data);
// Vue.prototype.$axios = axios;
// 方案二:Vue.prototype.$axios = request;
// 方案三:安装插件(项目中最常用)
Vue.use(gitHub);
}
}).$mount("#app");vue
<!-- src/App.vue -->
<template>
<div>
<h3>App</h3>
<h3 v-if="isLoading">正在拼命加载中……</h3>
<h3 v-else-if="isError">产生异常,请稍后再试……</h3>
<template v-else>
<div v-for="item in items" :key="item.id">
<p>{{ item.full_name }}</p>
</div>
</template>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
items: [],
isLoading: true,
isError: false
};
},
async mounted() {
try {
const { items } = await this.$axios.get("/search/repositories", {
params: { q: "r", sort: "stars" }
});
this.items = items;
this.isLoading = false;
} catch (err) {
console.log(1111, err);
this.isLoading = false;
this.isError = true;
}
}
};
</script>
<style scoped></style>关键行与易错点
- 方案一:在
main.js的beforeCreate里直接配axios.defaults.baseURL、加拦截器、挂Vue.prototype.$axios。 - 方案二:把配置抽到
src/request/index.js,组件里不再import axios,改用this.$axios;模块只加载一次,缓存的是同一个实例,所以配置在别处改过,其它地方import到的也是改过的。 - 方案三:导出一个函数,
Vue.use(gitHub)时 Vue 会把构造函数传进来,函数里Vue.prototype.$axios = axios。转写稿里明确说三种都要看懂,实际项目中最常用第三种。 - 最严重的问题:不要在
index.js和gitHub.js里给同一个 axios 实例各加一次响应拦截器。第一个拦截器已经return res.data,第二个拦截器收到的res就是data本身,再写res.data就是undefined,组件里解构{ items }会直接报错,catch也不会进(因为没抛错而是解构出 undefined 后访问失败)。 - 配好
baseURL后,请求地址写相对路径"/search/repositories",不要重复写完整域名。 - 拦截器可以注册多个,但要注意执行顺序和每个拦截器的返回值,后一个拿到的
res是前一个的返回值。
示例三:搜索用户(视频 4、5)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>14-4 搜索用户</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
.box { width: 640px; margin: 0 auto; }
.user { display: flex; align-items: center; border-bottom: 1px solid #eee; padding: 6px; }
.user img { width: 50px; height: 50px; margin-right: 10px; }
</style>
</head>
<body>
<div id="app" class="box">
<h3>App</h3>
<input type="text" ref="keyword" placeholder="请输入搜索的关键词" />
<button @click="searchUser">搜索</button>
<!-- 还没有点击搜索 -->
<h3 v-if="!isSearched">您可以输入关键字,然后点击搜索按钮</h3>
<template v-else>
<h3 v-if="isError">您搜索的过于频繁,请稍后再试</h3>
<h3 v-else-if="isLoading">正在拼命加载中……</h3>
<h3 v-else-if="items.length === 0">搜索出来的结果为空</h3>
<div v-else>
<div class="user" v-for="item in items" :key="item.id">
<a :href="item.html_url" target="_blank">
<img :src="item.avatar_url" alt="" />
</a>
<p>{{ item.login }}</p>
</div>
</div>
</template>
</div>
<script>
new Vue({
el: "#app",
data() {
return {
items: [],
isSearched: false, // 是否点击了搜索按钮
isLoading: false, // 是否正在加载
isError: false // 是否有异常
};
},
methods: {
async searchUser() {
const q = this.$refs.keyword.value;
if (q.length === 0) {
alert("请输入搜索的关键字");
return; // 没有输入就不发请求
}
this.isSearched = true;
this.isLoading = true;
this.isError = false;
try {
const { items } = await axios.get(
"https://api.github.com/search/users",
{ params: { q, sort: "stars" } }
);
this.items = items;
} catch (err) {
console.log(err);
this.isError = true;
} finally {
this.isLoading = false; // 成功失败都要关掉 loading
}
}
}
});
</script>
</body>
</html>关键行与易错点
- 不能靠
items.length判断“加载中”:空数组也有length,接口返回空数据时就会误判,所以必须单独加isLoading状态。 - 判断顺序很关键:
isError要放在isLoading前面。如果只在try里把isLoading置为false,出错时它永远是true,用户只会看到“正在拼命加载中……”,看不到异常提示;这里用finally保证一定关闭。 isSearched默认false,用v-if="!isSearched"显示初始引导文案,点击按钮后置true,引导文案就不再出现。- 取输入框的值用
ref:this.$refs.keyword.value,长度为 0 时提示并return,不发请求。 v-for的:key用item.id;图片地址、跳转链接都要用:绑定(v-bind),写成src="item.avatar_url"会变成字面量字符串。- 请求参数写成对象
params: { q, sort: "stars" },q是搜索关键词;如果配置了baseURL,这里的地址改成相对路径"/search/users"。 - 网络异常(频繁刷新会被限流)会走
catch,此时要同时维护isError和isLoading两个状态,缺一个页面就卡住。
常见错误
模板里直接写
v-for="item in data.items",页面一片空白 现象:控制台没报错,但列表不渲染,或提示items不存在。 原因:data是函数,不是实例上可直接访问的响应式状态;模板只能取到实例代理出来的属性。 修法:先在data()里声明items: [],请求成功后this.items = data.items,模板只遍历items。const { items } = await this.$axios.get(...)解构出来是undefined现象:items为undefined,v-for不报错但什么都不显示,或读属性直接抛异常。 原因:响应拦截器返回什么,await拿到的就是什么;笔记里request/index.js与gitHub.js都挂了return res.data,同一模块被引入两次时拦截器叠加,第二次再取.data就取不到了。 修法:拦截器只保留一处return res.data;不确定时先const res = await ...打印res再决定解构层级,别凭记忆解构。拦截器写了却没
return现象:请求地址、参数都对,网络面板 200,但res是undefined。 原因:interceptors.response.use(res => { console.log(res) })没有返回值,返回值被吞成undefined。 修法:return res.data(或需要的层级),并记住多个拦截器是链式的,后一个拿到的是前一个的返回值。请求报错后一直卡在"正在拼命加载中……" 现象:接口 403/网络异常时,页面永远停在 loading,用户以为网坏了。 原因:只在
try成功分支里this.isLoading = false,catch里没关掉 loading。 修法:isLoading的关闭放进finally,catch里只负责this.isError = true;模板用v-if / v-else-if / v-else(循环用<template v-else>包一层)。用
items.length === 0当"加载中"的判断 现象:搜索无结果时永远显示加载中,或有结果时闪现空白。 原因:空数组长度也是 0,和"请求未完成"是两件事,不能用同一个信号表达。 修法:loading、error、结果为空三个状态各用独立布尔量/数据表示,模板里按"未搜索 → 加载中 → 异常 → 结果为空 → 有数据"的顺序排v-if分支。图片和链接写成了静态属性 现象:
<img src="item.avatar_url">、<a href="item.html_url">渲染出字面量字符串。 原因:没有:,Vue 当成普通字符串。 修法::src、:href、:key一律加冒号绑定。
练习
- 把
App.vue里的列表抽成RepoList.vue,父组件只负责发请求并传items,子组件用props接收渲染;要求父组件请求失败时子组件不渲染、由父组件展示错误提示。 - 给搜索案例补上"结果为空"和"搜索过于频繁"两个分支,并故意连续快速点击搜索按钮触发 403,验证
isError分支能正常显示、isLoading能正常关闭。 - 自己写两个响应拦截器(一个打印耗时,一个返回
res.data),在控制台验证第二个拦截器拿到的res到底是什么,并说明为什么顺序不能反。 - 把
isLoading / isError / isSearched三个状态合并成一个status: 'idle' | 'loading' | 'error' | 'empty' | 'success'的字段,重写模板分支,比较两种写法哪个更不容易漏状态。 - 给列表加一个"重新加载"按钮,点击后重置
isLoading = true、清空items再重新请求,观察v-for与:key在数据替换时的表现。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 渲染数据列表 | 06:38 |
| 2 | 完成加载中 | 06:18 |
| 3 | 封装 | 19:46 |
| 4 | 呈现静态页面 | 06:30 |
| 5 | 完成搜索 | 16:52 |
小结
- 模板只认实例上的响应式属性,
data不是数据本身,"先声明、后赋值"是固定动作。 await拿到的是拦截器的返回值,不是axios原始响应;拦截器不return就等于把数据吞了。- 同一个模块被多处
import共享同一份 axios 实例,配置和拦截器会累加——这是笔记里 422、undefined类问题的根因。 - loading、error、空结果、有数据是四个独立状态,不要用数据长度这种间接信号去推断请求进度。
- 关闭 loading 属于"无论成败都要执行"的动作,应放
finally,否则异常路径必然漏状态。 - 请求层封装的价值在于:组件里只关心"拿数据 + 改状态",URL、baseURL、解构层级统一收口到
request目录。 - 页面驱动靠数据,
items一变视图就变;排查渲染问题时,先console.log数据,再看模板,最后才怀疑样式。 - 接口报错要当成正常业务分支来设计(提示语、可重试),而不是当作不会发生的事故。