前端API模块方案

1,779 阅读5分钟

什么是前端API模块?

首先明确一个概念,本文说的前端API模块指的是什么?

在前端代码中(本文不包括BFF,BFF也划分到“后端”)存在请求后端服务接口代码,通常情况下,一个后端接口可能被多个页面使用,所以我们通常会把后端接口API相关的前端代码放在一个模块中统一管理,在需要发起请求的代码中,引用该模块,然后使用指定API的方法发起请求,从而达到复用代码、统一管理的目的,而这个管理API的模块,我将它命名为“前端API模块”。

因为我也不太清楚我们前端圈里怎么命名这东西,所以我姑且先这么称呼它吧。如果可以的话,在评论区告诉我这个应该怎么称呼更好😂!

历史方案

首先说一下目前项目的方案,以及存在的问题。

方案:每个服务管理独立的API模块

我们将一个大分类的功能模块称之为服务,比如产品服务、用户服务等,然后前后端都使用服务分类将代码划分为不同的目录或者微服务。

后端的接口通过微服务提供,比如产品服务的API地址为 /api/products/v1/xxx,用户服务的API地址为 /api/users/v1/yyy 等。

前端类似地在每个服务目录下,提供单独的apis模块,比如在products目录下,有一个 apis.js 文件导出apis模块,该模块定义所有后端products服务的API地址常量。在页面代码中发起请求时,通过引入该模块,然后使用该模块导出的常量名称,然后使用请求方法发起请求。

// ./products/apis.js

const VERSION = 'v1';
const SERVICE = 'products';

const PRODUCT_LIST = `/api/${SERVICE}/${VERSION}/list`;

export default {
  PRODUCT_LIST,
};
// ./products/pages/list.js

import APIS from '../apis';
import axios from 'axios';

const { PRODUCT_LIST } = APIS;

axios.get(PRODUCT_LIST, {
  params: {
    pageIndex: 1,
    pageSize: 10,
  }
}).then(() => {
  // ...
}).catch(() => {
  // ...
});

使用这种方案,当API接口地址发生变化时,只需要修改apis.js中的常量值即可。

问题

1、当多个服务使用相同的API时,需要修改多个服务的API,容易出现漏改情况;

2、当业务代码使用不规范时,没有按照规范引入API模块,修改API地址时,出现漏改情况;

3、业务代码还需要自行引入请求库,这些原本也可以内聚到API模块中实现;

改良方案

所以需要改良方案,解决历史方案存在的问题。

1、抽取到公共服务API模块或者独立的API服务

将所有的API都收起来,放在公共服务或者单独一个服务中进行管理,不再分散到每个服务中去。这样做的好处是,没有产品服务都使用同一个API服务,方便进行统一规范的管理,不管是API的调整,还是请求拦截,又或者是其他需要增强的功能,都只需要在一个API服务中进行变更,而不需要在每个产品服务中修改。这样,也就不会出现由于需要改动多处导致修改错误、漏改的问题。

当然,任何方案都不是完美的,方案的选择永远都是权衡的艺术。

这种方案的问题也会存在一些不尽人意的问题。

首先,它会导致公共资源加载成本和内存成本,会存在一定的冗余。公共资源加载时需要首先加载API模块代码,API模块导出的对象,需要长期占用内存空间。针对问题,我们可以通过按需加载的方式减少资源加载成本,以及利用缓存(sessionStorage等)将数据缓存下来,需要时再读取的方式减少内存成本。

其次,开发过程中增加了代码管理成本。当公共服务代码和产品服务代码分开代码库时,需要注意不同代码库的代码版本(通常指的是代码分支),否则可能会出现产品服务引用了历史版本的公共模块,而导致缺少相应API、或者API不争气的问题。

总的来说,新的方案是利大于弊的。

2、API模块提供更加完善的功能

之前的方案中,只是提供了API地址,功能很受限,所以新的方案中,我们不仅提供API地址,还需要把每个API作为一个完整的请求函数,提供默认的请求参数值,增强请求的容错性。

// ./apis/products.js

import axios from 'axios'; 

const VERSION = 'v1';
const SERVICE = 'products';

const PRODUCT_V1 = `/api/${SERVICE}/${VERSION}`;

// 获取产品列表接口
export function getProductList(params = {}) {
  return axios.get(`${PRODUCT_V1}/list`, {
    params: {
      // 默认参数
      pageIndex: 1,
      pageSize: 10,
      
      // 传入的参数
      ...params,
    }
  });
}

// 新增产品接口
export function addProduct(data) {
  if(!data || !data.name) {
    return Promise.reject('The [name] is required!');
  }
  return axios.post(`${PRODUCT_V1}`, {
      data: {
          ...data,
          name: data.name.trim(),
      }
  });
} 

// 修改产品接口
export function addProduct(params, data) {
  if(!params || !params.id) {
       return Promise.reject('The [id] in params is required!');
  }
  if(data && data.name) {
       return Promise.reject('The data.name is not allowed to be modified!');
  }
  return axios.put(`${PRODUCT_V1}/${params.id}`, {
      data,
  });
}  

// ./apis/index.js
import axios from 'axios'; 
export * as products from './products';

// 拦截请求
axios.interceptors.request.use(function (config) {
  // Do something before request is sent
  return config;
}, function (error) {
  // Do something with request error
  return Promise.reject(error);
});

// 拦截响应
axios.interceptors.response.use(function (response) {
  // Any status code that lie within the range of 2xx cause this function to trigger
  // Do something with response data
  return response;
}, function (error) {
  // Any status codes that falls outside the range of 2xx cause this function to trigger
  // Do something with response error
  return Promise.reject(error);
});
// ./products/pages/list.js
import { products } from './apis';

const { getProductList } = products;

getProductList()
  .then(() => {
  	// ...
    })
    .catch(() => {
  	// ...
    })