Skip to content

Vue3教程 - 22 网络请求

开发前端项目,网络请求必不可少,因为要请求后端服务器,获取数据,然后将数据展示在页面上。

22.1 代理配置

但是我们开发前后端分离的项目可能会遇到一个问题,就是跨域。

20.1.1 什么是跨域

跨域(Cross-Origin)是指浏览器的同源策略(Same-Origin Policy),同源策略是浏览器的一种安全策略,浏览器会限制了从一个域(域名、协议和端口相同)加载的文档或脚本去访问另一个不同域的资源。这种限制是为了防止一个网站恶意读取另一个网站的敏感信息。

以下 URL 都被认为是不同的源:

  • http://foooor.comhttp://www.foooor.com(子域名不同)
  • http://www.foooor.com:80http://www.foooor.com:8080(端口不同)
  • http://www.foooor.comhttps://www.foooor.com(协议不同,http和https)

在前后端分离的项目中,前端和后台服务不是在同一个项目中,这会导致前后端的IP地址、端口或域名不一致,从而产生跨域问题。具体来说,当前端页面尝试通过Ajax等技术向后端接口请求数据时,如果前后端不满足同源策略的要求,浏览器就会阻止这次请求,从而引发跨域问题。

例如,后端服务运行在一个端口上,而Vue前端服务运行在另一个端口上时,由于两者端口不同,因此不满足同源策略的要求。此时,当Vue前端尝试通过Ajax请求后端接口获取数据时,就会遇到跨域问题。

20.2.2 跨域问题的解决方法

1 JSONP

JSONP(JSON with Padding)是一种通过 <script> 标签加载 JSON 数据的方式,但它只支持 GET 请求。JavaScript跨域请求数据是不可以的,但是使用 <script> 标签跨域请求其他站点的JavaScript脚本是可以的。可以把数据封装到JavaScript脚本中,作为脚本中函数的参数,然后将脚本返回给客户端,客户端获取脚本后,立即执行脚本,就可以获取到函数的参数,也就是服务器传递的数据了,从而解决了跨域问题。

JSONP是一种比较老的方式,目前已经被 CORS 大部分取代。

2 CORS

这是一种在服务器端通过设置适当的 CORS 头来允许跨域请求。也就是服务器告诉浏览器,我允许被跨域来访问,不要拦截了。

例如可以通过在Spring Boot项目中添加CORS配置类来实现,允许前端页面的域名访问后端接口。

3 使用反向代理

通过配置反向代理,前端所有的请求都会发送到同一个域名,也就是 Nginx 接收所有的请求,包括前端静态页面和后端动态数据接口的请求,然后 Nginx 反向代理服务器再根据请求路径将后端请求转发到对应的后端服务器,这就避免了浏览器的跨域限制。

22.2 代理配置

如果服务器使用了 CORS 来解决跨域问题,那么前端就不需要什么额外的处理了,直接就可以请求了。

如果后端的服务没有 CORS 相关的配置,那么在实际的 Vue 开发项目中,当项目开发完成,我们会将 Vue 项目打包成静态资源(HTML/JS/CSS),然后将静态资源部署到 Nginx 下,然后需要配置 Nginx 进行反向代理,将 Vue 项目中后端的请求转发给后端的服务器,具体可参见 前后端分离与跨域

简单的说:也就是前端页面的请求和请求后端接口的请求都发送给 Nginx,这样协议/域名/端口 都相同,就不存在跨域问题了,但是 Nginx 在请求的 URL 上做一些过滤,例如 URL 中带 /api/ 的,Nginx 就将这些请求转发给后端的服务器。所以在 Vue 中所有请求后端服务器的接口,都统一带上 /api/ ,这样就可以了。

上面是开发完成部署到服务器上,可以使用上面的方式。


我们在本地进行开发的时候,不用那么麻烦,直接使用 vite 提供的代理服务来实现就可以了。

在项目 vite.config.js 中配置如下:

js
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue()
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    port: 3000,  // 如果想修改端口号,还可以在这里修改,前面一直使用的是5173
    
    // 为开发服务器配置自定义代理规则
    proxy: {
      '/api': {
        target: 'http://localhost:8090',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      }
    },
  }
})

通过上面的配置,代理服务器会拦截 /api 开头的路径,将请求转发给后端的 http://localhost:8090 服务器。

举个栗子:

  • 如果是 http://localhost:8080/login 请求,不是以 /api 开头,那么代理服务器不会拦截,而是由前端处理;
  • 如果是 http://localhost:8080/api/doLogin 请求,请求是以 /api 开头,代理服务器会拦截该请求,转发给后端服务器,因为代理服务器配置了 rewrite: (path) => path.replace(/^\/api/, '') ,那么会将 api 去掉,请求到后端的地址变为:http://localhost:8090/doLogin

通过上面的配置,我们在开发环境中就解决了跨域的问题。

代理是可以配置多个的:

js
server: {
  proxy: {
    // 第一个代理
    '/api': {
      target: 'http://localhost:8090',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, ''),
    },
    // 第二个代理
    '/demo': {
      target: 'http://localhost:10000',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/demo/, ''),
    },
  },
}

22.3 Axios

一般在Vue项目中,发起网络请求最常用的工具是 AxiosAxios 是一个基于 Promise 的HTTP客户端,可以在浏览器和Node.js环境中使用。它提供了简洁的API和强大的功能,如拦截请求和响应、取消请求、自动转换JSON数据等。

GitHub:https://github.com/axios/axios

中文文档:http://www.axios-js.com/

下面介绍一下如何使用 Axios。

1 安装Axios

项目下运行如下命令:

bash
# 使用npm
npm install axios

# 或者使用yarn
yarn add axios

2 创建请求工具类

一般在项目中,我们会编写一个 Axios 的工具类,然后在各个组件中引入并使用。

例如在 src/util 目录下创建一个 request.js,内容如下:

下面引入了 element-plus 用来错误信息提示,并引入了路由,用于跳转到登录页面。

服务器返回的消息内容格式为:{"code": 0, "message": "消息内容", "data": {数据}}

请求的时候,携带本地的token请求服务器。

js
// request.ts
// 引入axios
import axios, { type AxiosInstance, type InternalAxiosRequestConfig, type AxiosResponse } from 'axios';
// 引入router,用于页面跳转
import router from '@/router';
// 使用element-plus Message做消息提醒
import { ElMessage } from 'element-plus';

// 创建一个 Axios 实例
const instance: AxiosInstance = axios.create({
  baseURL: 'http://localhost:8080/api', // 修改为你的API基础URL
  timeout: 10 * 1000  // 请求超时时间
});

/**
  * 使用Message提示错误信息
  */
const showError = (msg: string) => {
  ElMessage({
    message: msg,
    type: 'error',
    duration: 2 * 1000,
  });
};

/**
  * 跳转登录页
  */
const toLogin = () => {
  router.replace({
    name: 'login'
  });
};

/**
  * 请求失败后的错误统一处理
  */
const errorHandle = (status: number | null, message: string) => {
  // 状态码判断
  switch (status) {
    case 401:  // 401 未登录状态,跳转登录页
      toLogin();
      break;
    case 403:  // 403 token过期,清除token并跳转登录页
      showError('登录过期,请重新登录');
      localStorage.removeItem('token');

      setTimeout(() => {
        toLogin();
      }, 1000);

      break;
    case 404:  // 404 请求不存在
      showError('请求的资源不存在');
      break;
    default:
      showError(message);
  }
};

// 请求拦截器
instance.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    // 在发送请求之前做些什么,例如添加认证令牌
    const token = localStorage.getItem('token'); // 从localStorage获取token
    if (token) {
      if (config.headers) {
        config.headers['Authorization'] = `Bearer ${token}`;
      }
    }
    return config;
  },
  (error: any) => {
    // 处理请求错误  
    console.error('发起请求出错:', error);
    return Promise.reject(new Error('发起请求出错'));
  });

// 响应拦截器
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    console.log('response:', response);
    // 对响应数据做些什么
    if (response.data && 0 === response.data.code) {
      console.log('response.data:', response.data);
      return response.data;
    } else {
      errorHandle(null, '请求出错');
      // 继续抛出错误
      return Promise.reject(new Error('请求出错'));
    }
  },
  (error: any) => {
    const status = error.response ? error.response.status : null;
    console.error('Response error:', error); // 打印错误信息
    errorHandle(status, '请求失败');
    // 继续抛出错误
    return Promise.reject(error);
  });

// 封装常用的HTTP方法
const httpUtils = {
  get<T = any>(url: string, params?: any): Promise<T> {
    return new Promise((resolve, reject) => {
      instance.get(url, { params })
        .then((res: AxiosResponse) => {
          resolve(res.data);
        }).catch(err => {
          console.error('get请求出错:', err);
          reject(err);
        });
    });
  },
  post<T = any>(url: string, data?: any): Promise<T> {
    return new Promise((resolve, reject) => {
      instance.post(url, data)
        .then((res: AxiosResponse) => {
          resolve(res.data);
        })
        .catch(err => {
          console.error('post请求出错:', err);
          reject(err);
        });
    });
  },
  put<T = any>(url: string, data?: any): Promise<T> {
    return new Promise((resolve, reject) => {
      instance.put(url, data)
        .then((res: AxiosResponse) => {
          resolve(res.data);
        })
        .catch(err => {
          console.error('put请求出错:', err);
          reject(err);
        });
    });
  },
  delete<T = any>(url: string, params?: any): Promise<T> {
    return new Promise((resolve, reject) => {
      instance.delete(url, { params })
        .then((res: AxiosResponse) => {
          resolve(res.data);
        })
        .catch(err => {
          console.error('delete请求出错:', err);
          reject(err);
        });
    });
  },
  upload<T = any>(url: string, formData: FormData): Promise<T> {
    return new Promise((resolve, reject) => {
      instance.post(url, formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        }
      }).then((res: AxiosResponse) => {
        resolve(res.data);
      }).catch(err => {
        console.error('upload请求出错:', err);
        reject(err);
      });
    });
  }
};

export default httpUtils;

22.4 简单的登录示例

下面来写一个简单的登录的功能,在登录页面输入密码跳转到首页,在首页调用后端接口获取用户信息。

1 准备请求类

封装一下请求服务器接口,调用 request.ts 发起网络请求,给后面的页面使用:

新家 src/api/user.ts

js
import httpUtils from '@/util/request'

// 登录
export function doLogin(data: any) {
    return httpUtils.post('/test/login', data);
}

// 获取用户信息
export function getUserInfo(id: string) {
    return httpUtils.get(`/test/user/${id}`)
}

2 页面发起网络请求

准备两个页面:

LoginPage.vue

点击登录后,发起登录请求,登录成功后,将 token 保存到本地。

vue
<template>
    <div class="login">
        <div>
            用户名:<input type="text" v-model="username" />
        </div>
        <div>
            密码:<input type="password" v-model="password" />
        </div>
        <div>
            <button @click="login">登录</button>
        </div>
    </div>
</template>

<script setup lang="ts">
import { doLogin } from '@/api/user'
import { ref } from 'vue';
import { useRouter } from 'vue-router';
let router = useRouter();

let username = ref('');
let password = ref('');

function login() {
    let data = {
        username: username.value,
        password: password.value,
    }
    doLogin(data).then((data) => {
        console.log('data:', data);

        // 保存token
        localStorage.setItem('token', data.token);

        // 跳转到首页
        router.replace({ name: 'home', query: { userId: data.userId } });
    });
}
</script>

HomePage.vue

进入到首页后,发起网络请求,获取用户信息。

vue
<template>
    <div class="home">
        <div>首页</div>
        <div> {{ userId }} </div>
        <div> {{ username }} </div>
        <div> {{ age }} </div>
    </div>
</template>

<script setup lang="ts">
import { getUserInfo } from '@/api/user';
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
let route = useRoute();

let userId = ref('');
let username = ref('');
let age = ref('');

onMounted(() => {
    loadUserInfo();
});

function loadUserInfo() {
    let id = route.query.userId as string;
    getUserInfo(id).then((data) => {
        console.log('user data:', data);
        userId.value = data.userId;
        username.value = data.username;
        age.value = data.age;
    });
}

</script>

App.vue

vue
<template>
  <div id="app">
    <RouterView />
  </div>
</template>

<!-- setup -->
<script lang="ts" setup>

</script>

3 路由配置

js
/* eslint-disable */
import { createRouter, createWebHistory } from "vue-router";
import HomePage from "@/pages/HomePage.vue";

// 配置路由
const routes = [
  {
    path: "/",
    redirect: "/home", // 重定向到首页
  },
  {
    path: "/home",
    name: "home",
    component: HomePage,
    meta: { requiresAuth: true, title: '首页' }, // 配置 meta,其中的属性可以自定义
  },
  {
    path: "/login",
    name: "login",
    component: () => import("@/pages/LoginPage.vue"),
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

/**
 * 前置路由守卫
 */
router.beforeEach((to, from, next) => {
  // 进行一些操作,比如检查用户是否已登录
  if (to.meta.requiresAuth && !isUserLoggedIn()) {
      next({ path: '/login' }) // 重定向到登录页
  } else {
      // 调用 next() 方法来放行,否则路由不会跳转  
      next();
  }
})

function isUserLoggedIn() {
  // 判断本地是否有token
  let token = localStorage.getItem('token');
  if (token) {
      return true;
  }
  else {
      return false;
  }
}

export default router;
内容未完......