Skip to content

渲染数据与加载中(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,避免同层混用导致结构混乱。异常分支里先关加载态,再显示错误态。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:渲染数据 + 加载中 / 异常状态(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 两个状态,缺一个页面就卡住。

常见错误 ​

  1. 模板里直接写 v-for="item in data.items",页面一片空白 现象:控制台没报错,但列表不渲染,或提示 items 不存在。 原因:data 是函数,不是实例上可直接访问的响应式状态;模板只能取到实例代理出来的属性。 修法:先在 data() 里声明 items: [],请求成功后 this.items = data.items,模板只遍历 items。

  2. 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 再决定解构层级,别凭记忆解构。

  3. 拦截器写了却没 return 现象:请求地址、参数都对,网络面板 200,但 res 是 undefined。 原因:interceptors.response.use(res => { console.log(res) }) 没有返回值,返回值被吞成 undefined。 修法:return res.data(或需要的层级),并记住多个拦截器是链式的,后一个拿到的是前一个的返回值。

  4. 请求报错后一直卡在"正在拼命加载中……" 现象:接口 403/网络异常时,页面永远停在 loading,用户以为网坏了。 原因:只在 try 成功分支里 this.isLoading = false,catch 里没关掉 loading。 修法:isLoading 的关闭放进 finally,catch 里只负责 this.isError = true;模板用 v-if / v-else-if / v-else(循环用 <template v-else> 包一层)。

  5. 用 items.length === 0 当"加载中"的判断 现象:搜索无结果时永远显示加载中,或有结果时闪现空白。 原因:空数组长度也是 0,和"请求未完成"是两件事,不能用同一个信号表达。 修法:loading、error、结果为空三个状态各用独立布尔量/数据表示,模板里按"未搜索 → 加载中 → 异常 → 结果为空 → 有数据"的顺序排 v-if 分支。

  6. 图片和链接写成了静态属性 现象:<img src="item.avatar_url">、<a href="item.html_url"> 渲染出字面量字符串。 原因:没有 :,Vue 当成普通字符串。 修法::src、:href、:key 一律加冒号绑定。

练习 ​

  1. 把 App.vue 里的列表抽成 RepoList.vue,父组件只负责发请求并传 items,子组件用 props 接收渲染;要求父组件请求失败时子组件不渲染、由父组件展示错误提示。
  2. 给搜索案例补上"结果为空"和"搜索过于频繁"两个分支,并故意连续快速点击搜索按钮触发 403,验证 isError 分支能正常显示、isLoading 能正常关闭。
  3. 自己写两个响应拦截器(一个打印耗时,一个返回 res.data),在控制台验证第二个拦截器拿到的 res 到底是什么,并说明为什么顺序不能反。
  4. 把 isLoading / isError / isSearched 三个状态合并成一个 status: 'idle' | 'loading' | 'error' | 'empty' | 'success' 的字段,重写模板分支,比较两种写法哪个更不容易漏状态。
  5. 给列表加一个"重新加载"按钮,点击后重置 isLoading = true、清空 items 再重新请求,观察 v-for 与 :key 在数据替换时的表现。

原视频对照 ​

#视频时长
1渲染数据列表06:38
2完成加载中06:18
3封装19:46
4呈现静态页面06:30
5完成搜索16:52

小结 ​

  1. 模板只认实例上的响应式属性,data 不是数据本身,"先声明、后赋值"是固定动作。
  2. await 拿到的是拦截器的返回值,不是 axios 原始响应;拦截器不 return 就等于把数据吞了。
  3. 同一个模块被多处 import 共享同一份 axios 实例,配置和拦截器会累加——这是笔记里 422、undefined 类问题的根因。
  4. loading、error、空结果、有数据是四个独立状态,不要用数据长度这种间接信号去推断请求进度。
  5. 关闭 loading 属于"无论成败都要执行"的动作,应放 finally,否则异常路径必然漏状态。
  6. 请求层封装的价值在于:组件里只关心"拿数据 + 改状态",URL、baseURL、解构层级统一收口到 request 目录。
  7. 页面驱动靠数据,items 一变视图就变;排查渲染问题时,先 console.log 数据,再看模板,最后才怀疑样式。
  8. 接口报错要当成正常业务分支来设计(提示语、可重试),而不是当作不会发生的事故。