跳转到内容

axios

标签「axios」下的 1 篇文章

Vue 请求后端数据与跨域问题

这篇文章迁移自我早年写在博客园的一篇记录。当时遇到的问题很直接:后端接口已经写好了,Vue 前端应该怎么请求数据?请求时报跨域又应该怎么处理?

现在回头看,这其实是前后端分离开发里非常典型的一步:前端项目跑在一个端口,后端服务跑在另一个端口,浏览器因为同源策略限制,默认不允许它们随便互相请求。

这篇文章按现在的写法重新整理一遍:先用 axios 发起请求,再抽出请求模块,最后处理跨域问题。

假设后端提供了一个用户列表接口:

GET http://localhost:8088/user

前端 Vue 项目运行在另一个地址,例如:

http://localhost:8080/

这两个地址的协议、域名或端口只要有一个不同,就不是同源。这里端口不同,所以浏览器会把它们视为跨域请求。

前端请求接口可以使用 axios

Terminal window
npm install axios

安装后,在需要请求数据的 Vue 页面中引入:

import axios from 'axios';

如果项目使用的是 Vue 2 或 Options API,可以先在 methods 中写一个简单请求方法。

export default {
methods: {
fetchUsers() {
axios
// 这里填写后端接口地址
.get('http://localhost:8088/user')
.then(function (response) {
// 请求成功后,后端返回的数据在 response.data 中
console.log(response.data);
})
.catch(function (error) {
// 请求失败时会进入 catch
console.log(error);
})
.finally(function () {
// 不管成功还是失败,finally 都会执行
console.log('请求结束');
});
},
},
};

页面上可以绑定一个按钮测试请求:

<button @click="fetchUsers">请求用户数据</button>

点击按钮后,打开浏览器控制台。如果后端接口正常,并且跨域已经允许,就能在控制台看到返回数据。

如果你更喜欢同步风格,也可以用 async / await

import axios from 'axios';
export default {
methods: {
async fetchUsers() {
try {
// await 会等待请求完成
const response = await axios.get('http://localhost:8088/user');
// 后端真正返回的数据通常放在 response.data
console.log(response.data);
} catch (error) {
// 网络错误、接口错误、跨域错误都会进入这里
console.log(error);
}
},
},
};

这种写法在业务代码里更容易读,尤其是请求成功后还有多步处理时。

刚开始写一个请求时,直接在页面里写完整地址没有问题。

但接口一多,就会出现几个问题:

  • 每个页面都要写 http://localhost:8088
  • 请求地址散落在各个组件里。
  • 后续换服务器地址时,需要到处改。
  • 请求头、错误处理、登录 token 等逻辑难以统一。

所以更常见的做法是:把 axios 实例单独封装起来,再把具体接口拆到 api 目录里。

可以按下面的方式组织前端请求代码:

  • 文件夹src/
    • 文件夹api/
      • user.js
    • 文件夹utils/
      • request.js
    • 文件夹views/
      • UserList.vue

这里的职责可以这样理解:

  • utils/request.js:创建 axios 实例,统一配置基础地址、超时时间、拦截器。
  • api/user.js:按业务模块封装用户相关接口。
  • views/UserList.vue:页面组件,只关心调用哪个接口,不关心底层请求细节。

src/utils/request.js 中写入:

import axios from 'axios';
const request = axios.create({
// 接口服务器地址
// 后续接口只需要写 /user、/login 这类路径
baseURL: 'http://localhost:8088',
// 超时时间,单位是毫秒
timeout: 10000,
});
// 请求拦截器:请求发出前会先经过这里
request.interceptors.request.use(
function (config) {
// 如果后续有 token,可以在这里统一添加
// config.headers.Authorization = `Bearer ${token}`;
return config;
},
function (error) {
return Promise.reject(error);
}
);
// 响应拦截器:后端响应后会先经过这里
request.interceptors.response.use(
function (response) {
// 这里先直接返回 response
// 如果后端有统一结构,也可以只返回 response.data
return response;
},
function (error) {
// 可以在这里统一处理错误提示、登录过期等问题
return Promise.reject(error);
}
);
export default request;

这样封装后,页面里就不需要每次都写完整后端地址了。

src/api/user.js 中写:

import request from '../utils/request';
export function getUserList(params = {}) {
return request({
// axios 里字段名是 method,不是 methods
method: 'GET',
// 会和 baseURL 拼接成 http://localhost:8088/user
url: '/user',
// GET 请求参数
params,
});
}

这里要注意一个细节:axios 配置里的请求方式字段是 method,不是 methods

在页面组件中引入接口方法:

import { getUserList } from '../api/user';
export default {
data() {
return {
tableData: [],
};
},
methods: {
async loadUsers() {
try {
const response = await getUserList();
// 如果响应拦截器返回的是完整 response,就从 response.data 取数据
this.tableData = response.data;
} catch (error) {
console.log(error);
}
},
},
};

如果希望页面创建时自动请求,可以在 created 生命周期中调用:

import { getUserList } from '../api/user';
export default {
data() {
return {
tableData: [],
};
},
created() {
// 页面创建时加载数据
this.loadUsers();
},
methods: {
async loadUsers() {
const response = await getUserList();
this.tableData = response.data;
},
},
};

如果要在模板中展示:

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

实际字段名要根据后端返回数据决定。

前端请求后端时,如果浏览器控制台出现类似错误:

Access to XMLHttpRequest at 'http://localhost:8088/user'
from origin 'http://localhost:8080'
has been blocked by CORS policy

这就是跨域问题。

它不是 axios 的问题,也不是 Vue 的问题,而是浏览器的同源策略在生效。

同源要求三者完全一致:

  • 协议相同,例如都是 http
  • 域名相同,例如都是 localhost
  • 端口相同,例如都是 8080

下面两个地址端口不同,所以不是同源:

前端:http://localhost:8080
后端:http://localhost:8088

浏览器会拦截前端 JavaScript 对后端的请求,除非后端明确允许这个来源访问。

如果后端是 Spring Boot,可以在 Controller 上添加 @CrossOrigin

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@CrossOrigin
public class UserController {
@GetMapping("/user")
public List<User> listUsers() {
return userService.listUsers();
}
}

这样写表示这个 Controller 允许跨域访问。

如果想限制来源,可以写得更明确:

@CrossOrigin(origins = "http://localhost:8080")
@RestController
public class UserController {
// ...
}

这比完全放开更安全。

如果接口很多,不想每个 Controller 都写 @CrossOrigin,也可以做全局配置。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
// 哪些接口路径允许跨域
.addMapping("/**")
// 允许哪个前端地址访问
.allowedOrigins("http://localhost:8080")
// 允许哪些请求方法
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 允许携带哪些请求头
.allowedHeaders("*");
}
}

开发阶段可以先这样配置。正式环境中,allowedOrigins 不建议随便写成 *,最好明确填写真实前端域名。

开发环境也可以通过前端代理解决跨域。

如果使用 Vue CLI,可以在 vue.config.js 中配置:

module.exports = {
devServer: {
proxy: {
'/api': {
// 后端服务地址
target: 'http://localhost:8088',
// 是否改变请求来源
changeOrigin: true,
// 把 /api/user 重写成 /user
pathRewrite: {
'^/api': '',
},
},
},
},
};

这时 axios 的基础地址可以改成:

const request = axios.create({
baseURL: '/api',
});

请求:

request({
method: 'GET',
url: '/user',
});

浏览器实际请求的是前端开发服务器:

http://localhost:8080/api/user

再由前端开发服务器转发给后端:

http://localhost:8088/user

这样浏览器看到的是同源请求,就不会触发跨域限制。

浏览器跨域是浏览器限制。你用 Postman、Apifox、curl 能请求成功,不代表浏览器里也能请求成功。

axios 报错是不是后端没返回数据

Section titled “axios 报错是不是后端没返回数据”

不一定。跨域错误时,请求可能已经被浏览器拦截,前端拿不到正常响应。要先看浏览器控制台的 Network 和 Console。

如果使用普通函数,this 可能发生变化。

getUserList().then(function (response) {
// 这里的 this 不一定是 Vue 组件实例
this.tableData = response.data;
});

可以改成箭头函数:

getUserList().then((response) => {
// 箭头函数不会重新绑定 this
this.tableData = response.data;
});

或者直接使用 async / await

Vue 请求后端接口时,可以按这个顺序处理:

  1. 使用 axios 先完成一次最简单的请求。
  2. 确认后端接口能正常返回数据。
  3. 如果浏览器报跨域,让后端配置 CORS,或者在开发环境使用代理。
  4. 当接口变多后,把 axios 封装成统一的 request 模块。
  5. 按业务拆分 API 文件,页面只调用方法,不直接拼接口地址。

这篇旧文当年只是为了解决“前端怎么拿到后端数据”这个问题。现在重新整理后,它更像是一条前后端分离开发的入门路径:先跑通,再封装,最后处理跨域和工程结构。

原文记录:vue请求后端数据和跨域问题