MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue3中基于Composition API处理网络请求

2021-09-252.8k 阅读

1. 理解 Composition API

在 Vue3 中,Composition API 是一项重大的更新,它为开发者提供了一种更灵活、高效的方式来组织和复用组件逻辑。传统的 Vue 组件使用选项式 API,将数据、方法、生命周期钩子等分散在不同的选项中。而 Composition API 则允许我们使用函数式的方式来组合逻辑,使代码结构更加清晰,可维护性更高。

1.1 基本概念

Composition API 主要包含以下几个核心概念:

  • reactive:用于创建响应式数据。通过 reactive 函数,可以将一个普通的 JavaScript 对象转换为响应式对象,Vue 会追踪对该对象属性的访问和修改,并触发相应的视图更新。例如:
import { reactive } from 'vue';

const state = reactive({
  count: 0
});
  • ref:同样用于创建响应式数据,与 reactive 不同的是,ref 可以创建一个包含任意类型值的响应式引用。当访问 ref 的值时,需要通过 .value 来获取,而在模板中使用时,Vue 会自动解包。示例如下:
import { ref } from 'vue';

const count = ref(0);
console.log(count.value); // 输出 0
count.value++;
  • computed:用于创建计算属性。计算属性会根据其依赖的响应式数据自动缓存,只有当依赖数据发生变化时才会重新计算。例如:
import { ref, computed } from 'vue';

const count = ref(0);
const doubleCount = computed(() => count.value * 2);
  • watch:用于监听响应式数据的变化。可以监听单个响应式数据,也可以监听多个响应式数据的变化,并在变化时执行相应的回调函数。例如:
import { ref, watch } from 'vue';

const count = ref(0);
watch(count, (newValue, oldValue) => {
  console.log(`值从 ${oldValue} 变为 ${newValue}`);
});
count.value++;

1.2 与选项式 API 的对比

选项式 API 在大型项目中可能会遇到一些问题,比如逻辑分散。当一个组件的功能越来越复杂时,数据、方法和生命周期钩子等选项会变得冗长且难以维护。而 Composition API 通过将相关逻辑封装到函数中,使代码更加模块化。例如,在选项式 API 中,数据和方法可能如下定义:

export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};

使用 Composition API 则可以写成:

import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };
    return {
      count,
      increment
    };
  }
};

可以看到,Composition API 使得逻辑更加集中,并且更容易复用。

2. 网络请求基础

在前端开发中,网络请求是获取和更新数据的重要手段。常见的网络请求方式有 fetchaxios

2.1 fetch

fetch 是现代浏览器提供的原生网络请求 API,它基于 Promise,使用起来相对简洁。以下是一个简单的 fetch 请求示例,用于获取 JSON 数据:

fetch('https://example.com/api/data')
 .then(response => response.json())
 .then(data => console.log(data))
 .catch(error => console.error('请求出错:', error));

fetch 的优点是原生支持,不需要额外引入库。但它也有一些不足之处,比如在处理错误时不够友好,默认不会处理 400 或 500 等错误状态码,需要手动检查 response.ok。例如:

fetch('https://example.com/api/data')
 .then(response => {
    if (!response.ok) {
      throw new Error('网络请求失败');
    }
    return response.json();
  })
 .then(data => console.log(data))
 .catch(error => console.error('请求出错:', error));

2.2 axios

axios 是一个流行的基于 Promise 的 HTTP 客户端,它可以在浏览器和 Node.js 中使用。axios 具有以下优点:

  • 简洁易用:语法简单,易于上手。
  • 支持多种请求方式:如 getpostputdelete 等。
  • 自动转换请求和响应数据:可以自动将请求数据序列化为 JSON 格式,将响应数据解析为 JavaScript 对象。
  • 丰富的拦截器功能:可以在请求发送前和响应接收后进行一些通用的处理,如添加请求头、处理错误等。

以下是一个使用 axios 发送 get 请求的示例:

import axios from 'axios';

axios.get('https://example.com/api/data')
 .then(response => console.log(response.data))
 .catch(error => console.error('请求出错:', error));

发送 post 请求的示例如下:

import axios from 'axios';

const data = {
  name: 'John',
  age: 30
};

axios.post('https://example.com/api/data', data)
 .then(response => console.log(response.data))
 .catch(error => console.error('请求出错:', error));

3. 在 Vue3 中使用 Composition API 处理网络请求

3.1 使用 axios 和 Composition API

首先,确保项目中已经安装了 axios。可以通过以下命令安装:

npm install axios

假设我们有一个组件,需要从后端获取用户列表数据。使用 Composition API 和 axios 可以这样实现:

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

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

const users = ref([]);

const fetchUsers = async () => {
  try {
    const response = await axios.get('https://example.com/api/users');
    users.value = response.data;
  } catch (error) {
    console.error('获取用户列表出错:', error);
  }
};

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

在上述代码中:

  • 使用 ref 创建了一个响应式数据 users,用于存储从后端获取的用户列表。
  • 定义了 fetchUsers 函数,该函数使用 axios 发送 get 请求获取用户数据。如果请求成功,将响应数据赋值给 users.value;如果请求失败,打印错误信息。
  • 使用 onMounted 生命周期钩子,在组件挂载后调用 fetchUsers 函数,从而触发网络请求。

3.2 处理加载状态和错误状态

为了提供更好的用户体验,我们需要处理网络请求的加载状态和错误状态。可以在组件中添加加载状态和错误信息的响应式数据:

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

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

const users = ref([]);
const loading = ref(false);
const error = ref(null);

const fetchUsers = async () => {
  loading.value = true;
  try {
    const response = await axios.get('https://example.com/api/users');
    users.value = response.data;
  } catch (error) {
    error.value = '获取用户列表出错';
  } finally {
    loading.value = false;
  }
};

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

在这段代码中:

  • 使用 ref 创建了 loadingerror 两个响应式数据,分别用于表示加载状态和错误信息。
  • fetchUsers 函数中,在请求开始时将 loading 设置为 true,请求结束后(无论成功或失败)将 loading 设置为 false。如果请求失败,将错误信息赋值给 error.value
  • 在模板中,根据 loadingerror 的值来显示加载指示器、错误信息或用户列表。

3.3 复用网络请求逻辑

在实际项目中,可能会有多个组件需要进行相似的网络请求。这时,可以将网络请求逻辑封装成一个可复用的函数。例如,我们创建一个 useUsers.js 文件,用于封装获取用户列表的逻辑:

import { ref, onMounted } from 'vue';
import axios from 'axios';

export const useUsers = () => {
  const users = ref([]);
  const loading = ref(false);
  const error = ref(null);

  const fetchUsers = async () => {
    loading.value = true;
    try {
      const response = await axios.get('https://example.com/api/users');
      users.value = response.data;
    } catch (error) {
      error.value = '获取用户列表出错';
    } finally {
      loading.value = false;
    }
  };

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

  return {
    users,
    loading,
    error
  };
};

然后在组件中使用这个复用函数:

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

<script setup>
import { useUsers } from './useUsers.js';

const { users, loading, error } = useUsers();
</script>

通过这种方式,将网络请求逻辑封装成一个自定义的组合函数,提高了代码的复用性,使得不同组件可以方便地使用相同的网络请求逻辑。

4. 处理复杂网络请求场景

4.1 带参数的网络请求

有时候,我们需要根据不同的参数来发送网络请求。例如,获取特定用户的详细信息,需要在请求 URL 中传递用户 ID。我们可以在复用的函数中添加参数支持。修改 useUsers.js 文件如下:

import { ref, onMounted } from 'vue';
import axios from 'axios';

export const useUser = (userId) => {
  const user = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const fetchUser = async () => {
    loading.value = true;
    try {
      const response = await axios.get(`https://example.com/api/users/${userId}`);
      user.value = response.data;
    } catch (error) {
      error.value = '获取用户信息出错';
    } finally {
      loading.value = false;
    }
  };

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

  return {
    user,
    loading,
    error
  };
};

在组件中使用时,可以传递不同的 userId 参数:

<template>
  <div>
    <loading v-if="loading"></loading>
    <div v-if="error" class="error">{{ error }}</div>
    <div v-else>
      <p>姓名: {{ user.name }}</p>
      <p>年龄: {{ user.age }}</p>
    </div>
  </div>
</template>

<script setup>
import { useUser } from './useUser.js';

const { user, loading, error } = useUser(1); // 获取 ID 为 1 的用户信息
</script>

4.2 并发网络请求

在某些场景下,我们可能需要同时发送多个网络请求,并在所有请求都完成后进行处理。例如,同时获取用户列表和用户总数。可以使用 Promise.all 来实现并发请求。修改 useUsers.js 文件如下:

import { ref, onMounted } from 'vue';
import axios from 'axios';

export const useUsersAndTotal = () => {
  const users = ref([]);
  const total = ref(0);
  const loading = ref(false);
  const error = ref(null);

  const fetchData = async () => {
    loading.value = true;
    try {
      const [usersResponse, totalResponse] = await Promise.all([
        axios.get('https://example.com/api/users'),
        axios.get('https://example.com/api/users/total')
      ]);
      users.value = usersResponse.data;
      total.value = totalResponse.data.count;
    } catch (error) {
      error.value = '获取数据出错';
    } finally {
      loading.value = false;
    }
  };

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

  return {
    users,
    total,
    loading,
    error
  };
};

在组件中使用这个函数:

<template>
  <div>
    <loading v-if="loading"></loading>
    <div v-if="error" class="error">{{ error }}</div>
    <div v-else>
      <p>用户总数: {{ total }}</p>
      <ul>
        <li v-for="user in users" :key="user.id">{{ user.name }}</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { useUsersAndTotal } from './useUsersAndTotal.js';

const { users, total, loading, error } = useUsersAndTotal();
</script>

在上述代码中,Promise.all 接收一个包含多个 Promise 的数组,只有当所有 Promise 都 resolved 时,才会继续执行 then 回调,并将所有 resolved 的值以数组形式返回。通过这种方式,我们可以方便地处理并发网络请求。

4.3 条件性网络请求

有时候,我们可能需要根据某些条件来决定是否发送网络请求。例如,只有在用户登录后才获取用户的个人信息。可以在组合函数中添加条件判断。修改 useUserInfo.js 文件如下:

import { ref, onMounted } from 'vue';
import axios from 'axios';

export const useUserInfo = (isLoggedIn) => {
  const userInfo = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const fetchUserInfo = async () => {
    if (!isLoggedIn) {
      return;
    }
    loading.value = true;
    try {
      const response = await axios.get('https://example.com/api/user/info');
      userInfo.value = response.data;
    } catch (error) {
      error.value = '获取用户信息出错';
    } finally {
      loading.value = false;
    }
  };

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

  return {
    userInfo,
    loading,
    error
  };
};

在组件中使用时,可以根据登录状态来决定是否获取用户信息:

<template>
  <div>
    <loading v-if="loading"></loading>
    <div v-if="error" class="error">{{ error }}</div>
    <div v-else>
      <p v-if="userInfo">姓名: {{ userInfo.name }}</p>
      <p v-if="userInfo">邮箱: {{ userInfo.email }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useUserInfo } from './useUserInfo.js';

const isLoggedIn = ref(true);
const { userInfo, loading, error } = useUserInfo(isLoggedIn.value);
</script>

通过在组合函数中添加条件判断,我们可以灵活地控制网络请求的发送,避免不必要的请求。

5. 优化网络请求性能

5.1 缓存数据

在一些情况下,我们可以对网络请求的数据进行缓存,以减少重复请求。例如,在短时间内多次获取相同的用户列表数据。可以使用一个简单的缓存机制,在组合函数中添加缓存逻辑。修改 useUsers.js 文件如下:

import { ref, onMounted } from 'vue';
import axios from 'axios';

let userCache = null;

export const useUsers = () => {
  const users = ref(userCache || []);
  const loading = ref(false);
  const error = ref(null);

  const fetchUsers = async () => {
    if (userCache) {
      return;
    }
    loading.value = true;
    try {
      const response = await axios.get('https://example.com/api/users');
      users.value = response.data;
      userCache = response.data;
    } catch (error) {
      error.value = '获取用户列表出错';
    } finally {
      loading.value = false;
    }
  };

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

  return {
    users,
    loading,
    error
  };
};

在上述代码中,使用了一个全局变量 userCache 来缓存用户列表数据。在每次请求前,先检查 userCache 是否有数据,如果有则直接返回,不再发送网络请求。这样可以有效地减少网络请求次数,提高性能。

5.2 节流和防抖

在一些场景下,用户可能会频繁触发网络请求,比如在搜索框中输入内容时实时搜索。这时,可以使用节流或防抖技术来控制请求频率。

防抖:防抖是指在一定时间内,如果事件被频繁触发,只执行最后一次。可以使用 lodash 库中的 debounce 函数来实现。首先安装 lodash

npm install lodash

假设我们有一个搜索组件,根据用户输入实时搜索用户。修改 useSearchUsers.js 文件如下:

import { ref, onMounted } from 'vue';
import axios from 'axios';
import { debounce } from 'lodash';

export const useSearchUsers = () => {
  const searchQuery = ref('');
  const users = ref([]);
  const loading = ref(false);
  const error = ref(null);

  const fetchSearchUsers = async () => {
    if (!searchQuery.value) {
      users.value = [];
      return;
    }
    loading.value = true;
    try {
      const response = await axios.get(`https://example.com/api/users/search?q=${searchQuery.value}`);
      users.value = response.data;
    } catch (error) {
      error.value = '搜索用户出错';
    } finally {
      loading.value = false;
    }
  };

  const debouncedFetch = debounce(fetchSearchUsers, 300);

  onMounted(() => {
    searchQuery.value = '';
  });

  return {
    searchQuery,
    users,
    loading,
    error,
    debouncedFetch
  };
};

在模板中使用:

<template>
  <div>
    <input v-model="searchQuery" @input="debouncedFetch" placeholder="搜索用户">
    <loading v-if="loading"></loading>
    <div v-if="error" class="error">{{ error }}</div>
    <ul v-else>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { useSearchUsers } from './useSearchUsers.js';

const { searchQuery, users, loading, error, debouncedFetch } = useSearchUsers();
</script>

在上述代码中,使用 debounce 函数对 fetchSearchUsers 函数进行包装,设置延迟时间为 300 毫秒。这样,当用户在搜索框中输入内容时,只有在停止输入 300 毫秒后才会触发网络请求,避免了频繁请求。

节流:节流是指在一定时间内,无论事件触发多么频繁,都只执行一次。同样可以使用 lodash 库中的 throttle 函数来实现。假设我们有一个滚动加载更多用户的场景,修改 useLoadMoreUsers.js 文件如下:

import { ref, onMounted } from 'vue';
import axios from 'axios';
import { throttle } from 'lodash';

export const useLoadMoreUsers = () => {
  const users = ref([]);
  const page = ref(1);
  const loading = ref(false);
  const error = ref(null);

  const fetchMoreUsers = async () => {
    if (loading.value) {
      return;
    }
    loading.value = true;
    try {
      const response = await axios.get(`https://example.com/api/users?page=${page.value}`);
      users.value = [...users.value, ...response.data];
      page.value++;
    } catch (error) {
      error.value = '加载更多用户出错';
    } finally {
      loading.value = false;
    }
  };

  const throttledFetch = throttle(fetchMoreUsers, 500);

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

  return {
    users,
    loading,
    error,
    throttledFetch
  };
};

在模板中使用:

<template>
  <div>
    <ul>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
    <button @click="throttledFetch" :disabled="loading">加载更多</button>
    <loading v-if="loading"></loading>
    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

<script setup>
import { useLoadMoreUsers } from './useLoadMoreUsers.js';

const { users, loading, error, throttledFetch } = useLoadMoreUsers();
</script>

在上述代码中,使用 throttle 函数对 fetchMoreUsers 函数进行包装,设置节流时间为 500 毫秒。这样,当用户点击“加载更多”按钮时,每 500 毫秒只会触发一次网络请求,避免了用户快速点击导致的频繁请求。

通过缓存数据、节流和防抖等技术,可以有效地优化网络请求性能,提升用户体验。

6. 处理网络请求中的安全性问题

6.1 防止 CSRF 攻击

CSRF(Cross - Site Request Forgery,跨站请求伪造)是一种常见的网络攻击方式,攻击者利用用户已登录的状态,在用户不知情的情况下,以用户的名义发送恶意请求。在 Vue 项目中,可以通过在请求头中添加 CSRF 令牌来防止这种攻击。

假设后端使用的是 Django 框架,Django 提供了内置的 CSRF 保护机制。在前端,我们需要在每次请求时将 CSRF 令牌添加到请求头中。可以在 axios 拦截器中统一处理:

import axios from 'axios';

const csrfToken = document.querySelector('meta[name="csrf - token"]').getAttribute('content');

axios.interceptors.request.use(config => {
  config.headers['X - CSRFToken'] = csrfToken;
  return config;
}, error => {
  return Promise.reject(error);
});

在 HTML 模板中,通常会有一个 meta 标签来存储 CSRF 令牌:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF - 8">
  <meta name="csrf - token" content="{{ csrf_token }}">
  <title>Vue with CSRF Protection</title>
</head>
<body>
  <div id="app"></div>
  <script src="app.js"></script>
</body>
</html>

通过这种方式,每次网络请求都会携带 CSRF 令牌,后端可以验证令牌的有效性,从而防止 CSRF 攻击。

6.2 处理敏感数据

在网络请求中,可能会涉及到敏感数据,如用户密码、银行卡信息等。对于这些敏感数据,一定要进行加密传输。常见的加密方式有使用 HTTPS 协议,它在传输层对数据进行加密,保证数据在传输过程中的安全性。

另外,在前端代码中,要避免直接在客户端存储敏感数据。如果确实需要存储一些临时的敏感信息,可以使用加密存储,比如使用 crypto - js 库对数据进行加密后存储在 localStorage 中。以下是一个简单的示例:

import CryptoJS from 'crypto - js';

// 加密数据
const sensitiveData = { password: '123456' };
const encryptedData = CryptoJS.AES.encrypt(JSON.stringify(sensitiveData), 'secretKey').toString();
localStorage.setItem('encryptedSensitiveData', encryptedData);

// 解密数据
const storedEncryptedData = localStorage.getItem('encryptedSensitiveData');
const bytes = CryptoJS.AES.decrypt(storedEncryptedData,'secretKey');
const decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));

通过加密传输和加密存储,可以有效地保护敏感数据的安全性。

6.3 防止 XSS 攻击

XSS(Cross - Site Scripting,跨站脚本攻击)是攻击者在网页中注入恶意脚本,当用户访问该网页时,恶意脚本会在用户浏览器中执行,从而窃取用户信息或进行其他恶意操作。在 Vue 中,由于模板引擎的特性,已经对大部分 XSS 攻击有了一定的防护。

然而,当我们需要动态渲染用户输入的内容时,比如用户发表评论,就需要特别小心。可以使用 DOMPurify 库来对用户输入进行净化,去除恶意脚本。首先安装 DOMPurify

npm install dompurify

假设我们有一个评论组件,在显示评论时使用 DOMPurify 进行净化:

<template>
  <div>
    <div v - for="comment in comments" :key="comment.id">
      <div v - html="sanitizedComment(comment.content)"></div>
    </div>
  </div>
</template>

<script setup>
import DOMPurify from 'dompurify';

const comments = ref([
  { id: 1, content: '这是一条正常评论' },
  { id: 2, content: '<script>alert("XSS")</script>' }
]);

const sanitizedComment = (content) => {
  return DOMPurify.sanitize(content);
};
</script>

在上述代码中,sanitizedComment 函数使用 DOMPurify 对评论内容进行净化,确保渲染到页面上的内容是安全的,从而防止 XSS 攻击。

通过采取这些措施,可以有效地处理网络请求中的安全性问题,保障用户数据的安全和应用的稳定运行。

在 Vue3 中使用 Composition API 处理网络请求,不仅可以使代码结构更加清晰,复用性更高,还能通过合理的优化和安全处理,提升应用的性能和安全性。希望通过本文的介绍,能帮助你更好地掌握这一技术,在实际项目中灵活运用。