组件接上数据:Vue 3 + Axios 请求页面实战

在前面的内容里,我们已经分别学过两件很重要的事:

  1. fetch / Ajax 主动向服务器发请求
  2. 用 Vue 3 的 refv-forv-ifonMounted 管理页面和数据

但真正进入 Vue 项目后,初学者最常见的断层恰恰发生在这里:

单独看 fetch 会,单独看 Vue 也会,可一旦把接口请求放进 Vue 组件里,就不知道数据该放哪、请求该什么时候发、加载状态怎么写、错误提示怎么处理。

这篇文章就是专门解决这个问题的。

本文不会一上来就讲拦截器、请求实例封装、接口模块化管理,而是先把最核心的一步走稳:

在 Vue 组件里,使用 axios 请求接口,并把返回数据渲染到页面上。


1. 为什么 Vue 项目里常配合 Axios

在浏览器里,请求接口可以直接使用原生 fetch。但在 Vue 工程项目中,很多团队更常使用 axios,原因通常有三个:

  1. 写法简洁:返回数据通常直接从 res.data 读取。
  2. 错误处理更直接:非 2xx 状态码通常会直接进入 catch
  3. 更适合后续工程化升级:请求拦截器、响应拦截器、统一 baseURL、统一错误提示都更容易接入。

所以对 Vue 项目的学习顺序来说,比较自然的路线是:

  1. 先理解浏览器原生 fetch
  2. 再学习 axios 在组件里的基础用法
  3. 最后再学习 axios 封装与接口模块化管理

2. 先安装 Axios

如果你已经有一个基于 Vite + Vue 3 的项目,可以直接安装:

npm install axios

安装完成后,就可以在组件里这样引入:

import axios from "axios";

3. 第一个请求:组件挂载后加载列表

在 Vue 组件中,请求接口最常见的时机就是 onMounted()

因为这个钩子表示:

组件已经挂载到页面上,可以开始做“页面初始化工作”了。

3.1 最小示例

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";

// 存放接口返回的用户列表
const users = ref([]);

async function getUserList() {
  // 发送 GET 请求获取列表
  const res = await axios.get("https://jsonplaceholder.typicode.com/users");
  // 把结果写回响应式状态
  users.value = res.data;
}

onMounted(() => {
  // 页面首次加载时拉取数据
  getUserList();
});
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

这段代码的流程非常重要:

  1. ref([]) 定义一个响应式数组
  2. getUserList() 中发起请求
  3. 把接口返回的数据赋值给 users.value
  4. 模板通过 v-for 自动渲染列表

这就是 Vue 项目里最基本的数据流:

请求数据 -> 保存到响应式状态 -> 模板自动更新


4. 为什么数据要放进 ref

很多初学者第一次把请求结果写进 Vue 组件时,容易这样写:

let users = [];

然后请求成功后:

users = res.data;

这样写在普通 JavaScript 中当然没有问题,但在 Vue 里,页面并不会因为这个普通变量变化就自动更新。

正确思路是:

const users = ref([]);
users.value = res.data;

因为 Vue 需要通过响应式系统感知数据变化,ref 正是为了这个目的而存在的。


5. 把页面状态写完整:加载中、空状态、错误状态

真实项目里,请求接口不能只考虑“成功”一种情况。

一个更完整的组件,至少要考虑这四种状态:

  • 初始状态
  • 加载中
  • 加载成功
  • 加载失败

如果返回的是列表,还要考虑:

  • 数据为空

5.1 一个更完整的列表组件

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";

const users = ref([]);
const loading = ref(false);
const errorMsg = ref("");

async function getUserList() {
  // 请求开始前先切到加载态
  loading.value = true;
  errorMsg.value = "";

  try {
    const res = await axios.get("https://jsonplaceholder.typicode.com/users");
    users.value = res.data;
  } catch (error) {
    // 请求失败时记录错误信息
    errorMsg.value = error.message || "请求失败";
  } finally {
    // 无论成功还是失败,都结束加载状态
    loading.value = false;
  }
}

onMounted(() => {
  getUserList();
});
</script>

<template>
  <div>
    <p v-if="loading">加载中...</p>
    <p v-else-if="errorMsg">{{ errorMsg }}</p>
    <p v-else-if="users.length === 0">暂无数据</p>

    <ul v-else>
      <li v-for="user in users" :key="user.id">
        {{ user.name }} - {{ user.email }}
      </li>
    </ul>
  </div>
</template>

这里最值得建立的意识是:

  • loading 控制加载中
  • errorMsg 控制失败提示
  • users.length === 0 控制空状态
  • users 真正负责业务数据

这些状态拆分清楚后,组件逻辑就会非常稳定。


6. 搜索与重新请求:不是只在 onMounted 里发一次

很多页面并不是只在首次进入时请求一次,还会有:

  • 搜索
  • 筛选
  • 切换页码
  • 手动刷新

这时要记住一个重要原则:

onMounted() 只是“首次触发入口”,真正发请求的逻辑应该抽成独立函数。

6.1 搜索示例

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";

const keyword = ref("");
const posts = ref([]);
const loading = ref(false);

async function getPostList() {
  loading.value = true;

  try {
    const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
    const list = res.data;

    // 根据关键字过滤标题
    posts.value = list.filter(item =>
      item.title.includes(keyword.value.trim())
    );
  } finally {
    loading.value = false;
  }
}

function handleSearch() {
  // 点击搜索时复用同一套请求逻辑
  getPostList();
}

onMounted(() => {
  getPostList();
});
</script>

<template>
  <div>
    <input v-model="keyword" placeholder="请输入关键字" />
    <button @click="handleSearch">搜索</button>

    <p v-if="loading">加载中...</p>

    <ul v-else>
      <li v-for="post in posts" :key="post.id">
        {{ post.title }}
      </li>
    </ul>
  </div>
</template>

这里的重点不是搜索逻辑本身,而是建立一个组件设计习惯:

  • 请求函数独立出来
  • 页面事件只负责调用请求函数
  • 初始化和后续刷新复用同一套逻辑

7. 提交表单:POST 请求怎么写

除了获取列表,Vue 页面里另一个高频操作就是提交表单。

例如注册、登录、新增文章、发布评论,本质上都是 POST 请求。

7.1 一个最小登录示例

<script setup>
import { reactive, ref } from "vue";
import axios from "axios";

// 表单数据适合用 reactive 统一管理
const formData = reactive({
  username: "",
  password: ""
});

const loading = ref(false);
const resultText = ref("");

async function handleSubmit() {
  loading.value = true;
  resultText.value = "";

  try {
    // 提交登录表单
    const res = await axios.post("https://api.example.com/login", {
      username: formData.username,
      password: formData.password
    });

    resultText.value = "提交成功";
    console.log("服务器返回:", res.data);
  } catch (error) {
    resultText.value = error.message || "提交失败";
  } finally {
    loading.value = false;
  }
}
</script>

<template>
  <div>
    <input v-model="formData.username" placeholder="用户名" />
    <input v-model="formData.password" type="password" placeholder="密码" />
    <button :disabled="loading" @click="handleSubmit">
      {{ loading ? "提交中..." : "登录" }}
    </button>

    <p>{{ resultText }}</p>
  </div>
</template>

这里体现了几个常见配合方式:

  • 表单数据适合用 reactive
  • 按钮加载状态适合用 ref
  • 请求完成后根据结果更新页面提示

8. Vue 页面里最常见的 Axios 写法

在企业项目中,一个普通的列表页经常会长成这样:

<script setup>
import { ref, reactive, onMounted } from "vue";
import axios from "axios";

const loading = ref(false);
const total = ref(0);
const tableData = ref([]);

// 统一维护列表筛选和分页参数
const queryParams = reactive({
  page: 1,
  pageSize: 10,
  keyword: ""
});

async function getTableList() {
  loading.value = true;

  try {
    // GET 请求里的查询条件放到 params 中
    const res = await axios.get("https://api.example.com/users", {
      params: queryParams
    });

    tableData.value = res.data.list;
    total.value = res.data.total;
  } catch (error) {
    console.error("获取表格数据失败:", error);
  } finally {
    loading.value = false;
  }
}

function handleSearch() {
  // 搜索时通常要重置回第一页
  queryParams.page = 1;
  getTableList();
}

function handlePageChange(newPage) {
  // 翻页时更新当前页并重新请求
  queryParams.page = newPage;
  getTableList();
}

onMounted(() => {
  getTableList();
});
</script>

这个结构非常值得记住,因为它几乎就是后台管理页面的基础模板:

  • queryParams 存筛选条件
  • tableData 存列表数据
  • total 存总数
  • loading 控制加载态
  • getTableList() 统一负责请求

你后面学 Element Plus 表格、分页、搜索表单时,会反复看到这种组织方式。


9. 常见请求配置:paramsdataheaders

9.1 params

用于 GET 请求的查询参数:

axios.get("/users", {
  params: {
    page: 1,
    keyword: "vue"
  }
});

最终会变成类似:

/users?page=1&keyword=vue

9.2 data

用于 POSTPUT 等请求体数据:

axios.post("/users", {
  username: "alice",
  password: "123456"
});

9.3 headers

用于附加请求头:

axios.get("/profile", {
  headers: {
    Authorization: "Bearer your-token"
  }
});

在当前阶段,你只需要先知道它们各自的职责。后面讲 Axios 封装 时,再把这些配置统一接管起来。


10. 初学者最容易踩的坑

10.1 忘记给响应式变量赋 .value

users = res.data; // 错误
users.value = res.data; // 正确

10.2 请求写在模板外,但没有在生命周期里触发

如果没有在 onMounted() 或点击事件里调用,请求函数根本不会执行。

10.3 接口返回成功,但页面没显示

常见原因:

  • 数据没有赋值给响应式变量
  • v-for 绑定字段名写错
  • 返回数据结构和你想象的不一样

这时第一件事永远是:

console.log(res.data);

10.4 只写成功分支,不写失败分支

页面一旦网络异常,就会直接白掉,用户完全不知道发生了什么。

10.5 把所有请求逻辑都塞进模板组件里

入门阶段这样写可以接受,但一旦页面变复杂,就要开始考虑:

  • 请求函数拆分
  • 复用逻辑抽离
  • 接口统一管理

这也正是后面学习 Axios 封装 的原因。


11. 当前这篇解决了什么,下一篇又解决什么

这篇文章重点解决的是:

  • Vue 组件里怎么发请求
  • 请求结果怎么进响应式状态
  • 列表和表单怎么和接口联动
  • 加载、错误、空状态怎么写

但真实项目里,下面这些问题我们还没解决:

  • baseURL 每次都手写吗
  • token 每次都手动加到请求头吗
  • 401 失效要不要统一跳登录
  • 接口路径要不要统一放到 api/ 目录
  • 错误提示要不要统一处理

这些就属于更高一层的工程化问题。


12. 小结

axios 接进 Vue 组件后,你就真正迈进了“页面连上真实数据”的阶段。

最核心的心智模型只有一句话:

axios 获取数据,用 Vue 的响应式状态保存数据,再用模板把状态渲染成页面。

一旦这个链路打通,后面学表格页、搜索页、表单页、后台管理系统,都会顺很多。


13. 下篇可以学什么

最适合接在这一篇之后的主题,就是:

13.1 Axios 封装与 API 管理

继续学习:

  • 创建统一请求实例
  • 配置 baseURL
  • 请求拦截器与响应拦截器
  • Token 自动注入
  • 统一错误处理
  • api/ 目录模块化管理

13.2 Vue Router 与 Pinia

继续学习:

  • 页面跳转与路由配置
  • 单页应用结构
  • 全局状态管理

如果你想把请求从“能用”升级到“可维护”,建议下一篇优先学习 Axios 封装与 API 管理