手写 Axios 源码(使用 TypeScript)

126 阅读3分钟

介绍

Axios 是一个流行的基于 Promise 的 JavaScript HTTP 客户端,用于在浏览器和 Node.js 中发送 HTTP 请求。本文将引导您使用 TypeScript 手写 Axios 的简化版本,以帮助您更好地理解其内部工作原理。

环境设置

首先,确保您的开发环境中已安装 Node.js 和 TypeScript。使用以下命令进行安装:

npm install -g typescript

项目初始化

创建一个新的文件夹并进入该文件夹:

mkdir axios-clone
cd axios-clone

使用以下命令初始化 TypeScript 项目:

tsc --init

这将在项目根目录中生成一个 tsconfig.json 文件。

创建入口文件

在项目根目录中创建一个名为 index.ts 的文件,这将是我们的 Axios 入口文件。

import Axios from "./axios";
import { AxiosInstance } from "./types";
import {CancelToken,isCancel} from './cancel';
// 可以创建一个axios实例
// 定义一个类的时候,一个类的原型,Axios.prototype 一个类的实例
function createInstance() :AxiosInstance{
​
    let context:Axios<any> = new Axios();
    let instance = Axios.prototype.request.bind(context);
​
    instance = Object.assign(instance, Axios.prototype, context);
​
    return instance as AxiosInstance;
​
​
  }
​
​
  let axios = createInstance();
  axios.CancelToken = new CancelToken();
  axios.isCancel = isCancel;
  export default axios;
​
  export * from './types';

定义Axios类

这段代码是一个使用TypeScript编写的简化版Axios类。Axios是一个流行的HTTP请求库,用于在浏览器和Node.js中发送HTTP请求。以下是这段代码的介绍:

export default class Axios<T> {
  public defaults: AxiosRequestConfig = defaults;
  public interceptors = {
    request: new AxiosInterceptorManager<AxiosRequestConfig>(),
    response: new AxiosInterceptorManager<AxiosResponse<T>>()
  }
​
  /**
   * 发起一个HTTP请求
   * @param config 请求配置
   * @returns Promise 包含请求配置或响应对象的Promise
   */
  request(config: AxiosRequestConfig): Promise<AxiosRequestConfig | AxiosResponse<T>> {
    // 对请求配置进行处理
    // 合并默认headers和传入的headers
    config.headers = { ...this.defaults.headers, ...config.headers };
    // 如果定义了请求数据的转换函数,对数据进行转换
    if (config.transformRequest && config.data) {
      config.data = config.transformRequest(config.data, config.headers);
    }
​
    // 构建拦截器链
    const chain: Interceptor<any>[] = [{
      onFulfilled: this.dispatchRequest,
      onRejected: undefined
    }];
​
    // 添加请求拦截器到拦截器链
    this.interceptors.request.interceptor.forEach(interceptor => {
      interceptor && chain.unshift(interceptor);
    });
​
    // 添加响应拦截器到拦截器链
    this.interceptors.response.interceptor.forEach(interceptor => {
      interceptor && chain.push(interceptor);
    });
​
    let promise: Promise<any> = Promise.resolve(config);
​
    // 依次执行拦截器链中的拦截器
    while (chain.length) {
      const { onFulfilled, onRejected } = chain.shift()!;
      promise = promise.then(onFulfilled, onRejected);
    }
​
    return promise;
  }
​
  /**
   * 发送请求的方法
   * @param config 请求配置
   * @returns Promise 包含响应对象的Promise
   */
  dispatchRequest<T>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return new Promise<AxiosResponse<T>>((resolve, reject) => {
      // 创建XMLHttpRequest对象
      const request = new XMLHttpRequest();
      // 获取请求配置中的信息
      let { method, url, params, headers, data, timeout } = config;
​
      // 处理URL中的查询参数
      if (params) {
        let paramsStr = Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
        url += (url!.indexOf('?') === -1 ? '?' : '&') + paramsStr;
      }
​
      request.open(method!, url!, true);
      request.responseType = "json";
​
      // 监听请求状态变化
      request.onreadystatechange = function () {
        if (request.readyState === 4) {
          if (request.status >= 200 && request.status <= 300) {
            // 构造响应对象
            let response: AxiosResponse<T> = {
              data: request.response ? request.response : request.responseText,
              status: request.status,
              statusText: request.statusText,
              config,
              request: request
            };
​
            // 如果定义了响应数据的转换函数,对响应数据进行转换
            if (config.transformResponse) {
             response = config.transformResponse(response);
            }
              resolve(response);
          } else {
           reject(`Error: Request failed with status code ${request.status}`);
          }
        }
      };
        
          // 设置请求头
  if (headers) {
    for (let key in headers) {
      if (key === 'common' || allMethods.includes(key)) {
        if (key === 'common' || key === config.method) {
          for (let key2 in headers[key]) {
            request.setRequestHeader(key2, headers[key][key2]);
          }
        }
      }
    }
  }
​
  let body: string | null = null;
​
  // 发送请求数据
  if (data) {
    request.send(data);
    body = JSON.stringify(data);
  }
​
  // 设置超时时间
  if (timeout) {
    request.timeout = timeout;
    request.ontimeout = function () {
      reject(`Timeout of ${timeout} ms exceeded`);
    }
  }
​
  // 处理请求取消
  if (config.CancelToken) {
    config.CancelToken.then((message: string) => {
      request.abort();
      reject(message);
    });
  }
​
  // 处理请求错误
  request.onerror = function () {
    reject("net::ERR_INTERNET_DISCONNECTED");
  };
​
  request.send(body);
});
​

定义CancelToken类

    export class Cancel{
      message?:string;
      constructor(message?:string){
        this.message = message;
      }
    }
    ​
    export function isCancel(value:any){
      return value instanceof Cancel;
    }
    ​
    ​
    export class CancelToken{
      public resolve: any;
      source(){
        return {
          token: new Promise((resolve) => {
            this.resolve = resolve;
          }),
          cancel: (message:string)=>{
            this.resolve(new Cancel(message));
          }
        }
      }
    }

定义defaults接口

    let defaults:AxiosRequestConfig = {
      method: "get",
      timeout: 0,
      headers: {
        common: {
          Accept: "application/json,text/plain,*/*"
        }
      },
      transformRequrest: function (data: any, headers: any): any {
        headers["common"]["Content-Type"] = "application/json";
        return JSON.stringify(data);
      },
    ​
      transformResponse:(response:any) => {
        return response.data;
      }
    ​
    };
    ​
    let getStyleMethods = ["get", "head", "delete", "options"];
    getStyleMethods.forEach(method => {
      defaults.headers![method] = {};
    });
    ​
    let postStyleMethods = ["post", "put", "patch"];
    postStyleMethods.forEach(method => {
      defaults.headers![method] = {
        "Content-Type": "application/x-www-form-urlencoded"
      };
    });
    ​
    let allMethods = [...getStyleMethods, ...postStyleMethods];
    ​

完善类型定义

types.ts 文件中,我们需要定义 AxiosRequestConfigAxiosResponse 的类型。


    import AxiosInterceptorManager from './axiosInterceptorManager';
    export type Methods = 'get' | 'GET' | 'post' | 'POST' | 'put' | 'PUT' | 'delete' | 'DELETE' | 'options' | 'OPTIONS' | 'head' | 'HEAD' | 'patch' | 'PATCH';
    export interface AxiosRequestConfig{
      url?: string;
      method?: Methods;
      params?: any;
      headers?: Record<string,any>;
      data?: any;
      timeout?: number;
      transformRequrest?:(data:any,hedaers:any)=>any;
      transformResponse?:(data:any)=>any;
      CancelToken?:any;
    }
    export interface AxiosInstance{
      <T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
      interceptors: {
        request: AxiosInterceptorManager<AxiosRequestConfig>;
        response: AxiosInterceptorManager<AxiosResponse>;
      },
      CancelToken?:any;
      isCancel?:any;
    ​
    }
    ​
    ​
    export interface AxiosResponse<T = any>{
      data: T;
      status: number;
      statusText: string;
      headers?: Record<string,any>;
      config?: AxiosRequestConfig;
      request?: XMLHttpRequest;
    }

定义AxiosInterceptorManager类

interface onFulfilled<V>{
      (value:V):V | Promise<V>;
    }
    interface onRejected{
      (error:any):any;
    }
    ​
    ​
    export interface Interceptor <V>{
      onFulfilled?: onFulfilled<V>;
      onRejected?: onRejected;
    }
    ​
    export default class AxiosInterceptorManager<V> {
      public interceptor:Array<Interceptor<V> | null> = []
      // 用来存储拦截器
      use(onFulfilled?:onFulfilled<V>,onRejected?:onRejected):number{
        this.interceptor.push({
          onFulfilled,
          onRejected
        })
        return this.interceptor.length - 1;
      }
    ​
      eject(id:number):void{
        if(this.interceptor[id]){
          this.interceptor[id] = null;
        }
      }
    }

测试代码

在项目根目录下创建一个 test.ts 文件,编写测试代码:

    import axios from './index'const baseUrl = 'https://api.wmdb.tv/api/v1/top';
    ​
    interface Data{
      type:string,
      skip:number,
      limit:number,
      lang:string,
      name:string
    }
    let data:Data = {
      type:'Imdb',
      skip:0,
      limit:50,
      lang:'Cn',
      name:"loser"
    }
    axios.interceptors.request.use((config:AxiosRequestConfig)=>{
      config.params.name += 1
      return config;
    })
    ​
    axios({
      method:'get',
      url:baseUrl,
      params:data,
    }).then((res:AxiosResponse)=>{
      console.log(res);
    ​
    })