前端Restful风格api封装,为业务赋能

85 阅读2分钟

前言

Restful官网给出的答案可以看出,Restful具有无状态,表现力强和易于拓展的特性。今天就聊一聊Restful规范在前端的运用,我将以Typescript作为前端语言来实现 Restful API,以此来说明 Restful API 带给前端的便利。 在平时的开发过程中,首先要关注的就是前后端的接口请求;但是常常会为如何命名而苦恼;例如在用户模块中,要获取列表,我们可能要命名为 GetUserList ,但是在权限模块中要获取列表,命名就可能就是 GetAuthorityList;在一个完整的业务模块中至少得有CRUD四个方法,如果要实现一整个项目,将会有非常多的代码和文件去实现。不遵循Restful规范几乎没有办法去减少这些代码,因为每一个函数的请求路径不一致。所有我们就要提到今天的主角Restful,目的就是减少重复写请求的工作,减少因为眼花和敲错导致懵逼的情况。

封装

下面的代码就是我对业务请求的一些封装思路,遵循了Restful规范再也不用 Alt + 单机找文件方法了。

export class HttpClient {
  constructor(config?: CreateAxiosDefaults) {
    this.instance = axios.create(config);
  }

  public get<T = any>(url: string, config?: AxiosRequestConfig) {
    return this.instance.get<T>(url, config);
  }
  
  public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
    return this.instance.post<T>(url, data, config);
  }

  public delete<T = any>(url: string, config?: AxiosRequestConfig) {
    return this.instance.delete<T>(url, config);
  }

  public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
    return this.instance.put<T>(url, data, config);
  }

  public patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
    return this.instance.patch<T>(url, data, config);
  }
}

expost const httpClient = new HttpClient({...})

继承

在这个类中,我们开始对增删改查的业务请求进行一层封装;为了贴合常见的业务,实现了分页查询、单个查询、删除、修改以及新增的请求函数,分别对应到RESTFul API的 GET、DELETE、PUT、POST 方法

import { AxiosPromise, CreateAxiosDefaults } from 'axios';
import { httpClient } from '../utils';

interface Page {
  page: number;
  pageSize: number;
  keyword?: string;
  all?: boolean;
}

interface PageResult<D = any> {
  page: number;
  pageSize: number;
  records: D[];
  total: number;
}

interface BaseApiContructConfig extends Omit<CreateAxiosDefaults, ''> {
  prefix: string;
}

interface BaseApiConstructOptions<D, T> {
  getOne: (id: number) => AxiosPromise<D>;
  getAll: (page: Page) => AxiosPromise<PageResult<D> | Pick<PageResult<D>, 'records'>>;

  updateOne: (id: number, data: T) => AxiosPromise<any>;
  deleteOne: (id: number) => AxiosPromise<any>;
  addOne: (data: T) => AxiosPromise<any>;
}

function pathJoin(path: string) {
  if (path.indexOf('/') === 0) {
    path = path.substring(1);
  }
  return path;
}

export class BaseApi<Data = any, Param = any> implements BaseApiConstructOptions<Data, Param> {
  private prefix: string;
  private instance: string;

  constructor(config: BaseApiContructConfig) {
    this.prefix = pathJoin(config.prefix);
  }

  getOne = (id: number) => {
    return httpClient.get(`/${this.prefix}/${id}`);
  };

  getAll = (page: Page) => {
    return httpClient.get<PageResult<Data> | Pick<PageResult<Data>, 'records'>>(`/${this.prefix}`, { params: page });
  };

  deleteOne = (id: number) => {
    return httpClient.delete(`/${this.prefix}/${id}`);
  };

  addOne = (data: Param) => {
    return httpClient.post(`/${this.prefix}`, { data });
  };

  updateOne = (id: number, data: Param) => {
    return httpClient.put(`/${this.prefix}/${id}`, { data });
  };
}

到具体模块中,只需要传递这个 prefix,代表了这个请求的路径,每一个请求都遵循这个路径,如果要查找具体资源,那么在路径后面的id进行查找,无具体资源是不用关心路径的,这样来看是不是代码简单了许多,也许有人会问如果这个模块不只有CRUD的业务呢?别忘了每一个子类可以写另外的方法。

import { BaseApi } from './base';

class ArticleApi extends BaseApi {}

export const articleApi = new ArticleApi({ prefix: '/articles' });

------------------------------------------------------------------

import { BaseApi } from './base';

class ArticleApi extends BaseApi {
    otherMethod = () => {
        return ...
    }
}

export const articleApi = new ArticleApi({ prefix: '/articles' });

希望在前端请求的封装上面提供一种不同的思路。