构建灵活的 Axios 客户端
发布日期:作者: Paul Redmond
最近,我开始改进我在 Vue 应用程序中使用 API 的方式,通过构建一个灵活的 Axios 客户端,我可以在 Vuex actions 和一次性组件中使用它。
我更喜欢构建特定 API 的 JS 模块,我可以将它们导入到我的组件和 Vuex 模块中,而不是从组件中内置 Axios 调用。构建 API 资源模块使我能够抽象化使用 HTTP 资源,并为常见模式提供便利方法。让我们看看几个例子!
入门
我将定义一些我的 API 客户端通常需要满足的条件
- 与错误报告挂钩(例如,Sentry)
- 能够传递 HTTP 头部,例如
Authorization
头部 - 能够在需要时利用我的 Vuex store(例如,获取用户的 JWT)
- 资源特定方法,简化了常见 API 调用的方式(例如,
users.find(userId)
)
我们编写的 API 客户端代码适用于您可能构建的任何 HTTP 客户端,但在本例中,我们将使用 Axios。
客户端
client.js
文件将是其他 HTTP 客户端用来创建新的 Axios 实例的基础模块。我们还将为请求和响应设置 Axios 所谓的“拦截器”。
请求拦截器接受一个请求配置和一个错误回调。您可以在此时自定义配置,但对于初学者,我们将返回相同的配置。错误回调将通过 Sentry 的 Raven
捕获异常
client.interceptors.request.use( requestConfig => requestConfig, (requestError) => { Raven.captureException(requestError); return Promise.reject(requestError); },);
响应拦截器接受两个参数,第一个参数是响应回调,第二个参数是响应错误
client.interceptors.response.use( response => response, (error) => { if (error.response.status >= 500) { Raven.captureException(error); } return Promise.reject(error); },);
以下是如何构建自己的基础 Axios 客户端的完整示例,该客户端利用 Sentry,检查 Vuex store 中是否有用户访问令牌,并提供一个 ApiClient
类,您可以使用它来构建自己的客户端。
import axios from 'axios';import Raven from 'raven-js';import store from '../store/index'; /** * Create a new Axios client instance * @see https://github.com/mzabriskie/axios#creating-an-instance */const getClient = (baseUrl = null) => { const options = { baseURL: baseUrl }; if (store.getters['users/isAuthenticated']) { options.headers = { Authorization: `Bearer ${store.getters['users/accessToken']}`, }; } const client = axios.create(options); // Add a request interceptor client.interceptors.request.use( requestConfig => requestConfig, (requestError) => { Raven.captureException(requestError); return Promise.reject(requestError); }, ); // Add a response interceptor client.interceptors.response.use( response => response, (error) => { if (error.response.status >= 500) { Raven.captureException(error); } return Promise.reject(error); }, ); return client;}; class ApiClient { constructor(baseUrl = null) { this.client = getClient(baseUrl); } get(url, conf = {}) { return this.client.get(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); } delete(url, conf = {}) { return this.client.delete(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); } head(url, conf = {}) { return this.client.head(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); } options(url, conf = {}) { return this.client.options(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); } post(url, data = {}, conf = {}) { return this.client.post(url, data, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); } put(url, data = {}, conf = {}) { return this.client.put(url, data, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); } patch(url, data = {}, conf = {}) { return this.client.patch(url, data, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); }} export { ApiClient }; /** * Base HTTP Client */export default { // Provide request methods with the default base_url get(url, conf = {}) { return getClient().get(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); }, delete(url, conf = {}) { return getClient().delete(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); }, head(url, conf = {}) { return getClient().head(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); }, options(url, conf = {}) { return getClient().options(url, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); }, post(url, data = {}, conf = {}) { return getClient().post(url, data, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); }, put(url, data = {}, conf = {}) { return getClient().put(url, data, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); }, patch(url, data = {}, conf = {}) { return getClient().patch(url, data, conf) .then(response => Promise.resolve(response)) .catch(error => Promise.reject(error)); },};
默认导出公开您可以直接使用 Axios 的所有 HTTP 方法。针对一次性请求的简单示例如下所示
import client from './client'; client.get('/users').then((response) => { // do stuff});
现在您拥有一个用于一次性请求的简单客户端,或者您可以将其用作构建更有趣的 HTTP 客户端的基础。
client.js
创建 API 客户端
使用 使用上面的客户端,您现在可以构建自己的 API 特定 Axios 客户端。
例如,假设您有一个 /users
资源,您可以构建特定的方法来管理用户
import { ApiClient } from '../client'; let client = new ApiClient(); export default { all() { return client.get('/users'); }, find(userId) { return client.get(`/users/${userId}`); }, update(userId, data) { return client.put(`/users/${userId}`, data); } }
上面的示例很简单,但您可以在收到响应后通过链接 promise 调用来进行进一步处理。例如,您可能只发送响应中需要的相关属性。或者,您可能没有访问后端的权限,您可以使用响应构建自定义对象或添加额外的属性,例如,如果您只收到 firstName
和 lastName
,那么可以添加一个 fullName
属性。Vue 也具有计算属性,但我希望您明白,您可以先格式化响应数据,然后再将其发送给消费者代码。
Vuex 用法
我建议您构建 HTTP 客户端模块的一个原因是,我们可以将它们拉到组件中,也可以在 Vuex store 中使用它们。相同的好处也适用,包括发送授权令牌以及避免直接在 Vuex 模块中进行 axios 调用。
以下是在 Vuex store 中获取用户并设置其个人资料数据的示例
import usersClient from './api/users'; // ... profile({ commit }, userId) { usersClient.find(userId) .then((response) => { let { firstName, lastName, } = response.data; commit('PROFILE', { firstName, lastName }); });},
改进
我认为一个明显的改进是,在创建新客户端时,允许通过回调进行更灵活的客户端配置。我展示的使用 Vuex 发送 bearer 令牌的方式是以一种不太灵活的方式嵌入到模块中的。
我希望您至少能理解我想要传达的想法。构建一个基础客户端 JS 模块来处理您可能希望在任何 HTTP 客户端上使用的底层细节很有帮助。例如,我们可以专注于创建有用的 API 客户端来管理用户和其他资源,而不是担心将 Sentry 连接到我们的请求和响应。
您是否还有其他 Axios 或 JavaScript API 客户端技巧?在 Twitter 上告诉我们 @laravelnews.