# Typescript 仿 axios 笔记

# 初始化

git clone https://github.com/alexjoverm/typescript-library-starter.git ts-axios
cd ts-axios
yarn install

目录文件介绍

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── code-of-conduct.md
├── node_modules
├── package-lock.json
├── package.json
├── rollup.config.ts // rollup 配置文件
├── src // 源码目录
├── test // 测试目录
├── tools // 发布到 GitHup pages 以及 发布到 npm 的一些配置脚本工具
├── tsconfig.json // TypeScript 编译配置文件
└── tslint.json // TypeScript lint 文件

根目录下创建 .prettierrc.json,根据个人喜好配置

{
  "tabWidth": 2,
  "useTabs": false,
  "endOfLine": "auto",
  "singleQuote": true,
  "semi": false,
  "trailingComma": "none",
  "bracketSpacing": true,
  "printWidth": 100
}

# 编写简单 Demo

src/index

export interface AxiosRequestConfig {
  url: string;
  method?: Method;
  data?: any;
  params?: any;
}

// 定义method取值范围
export type Method =
  | "get"
  | "GET"
  | "delete"
  | "Delete"
  | "head"
  | "HEAD"
  | "options"
  | "OPTIONS"
  | "post"
  | "POST"
  | "put"
  | "PUT"
  | "patch"
  | "PATCH";

export function axios(config: AxiosRequestConfig) {
  const { data, method = "get", url } = config;
  const XHR = new XMLHttpRequest();
  XHR.open(method, url, true);
  XHR.send(data);
}

# 需求实现

我们要做的 axios 的需求如下:

  • 在浏览器端使用 XMLHttpRequest 对象通讯
  • 支持 Promise API
  • 支持请求和响应的拦截器
  • 支持请求数据和响应数据的转换
  • 支持请求的取消
  • JSON 数据的自动转换
  • 客户端防止 XSRF

下面先开始从基础功能部分优化,基础功能部分包含以下:

  1. 处理请求 url 参数
  2. 处理请求 body 数据
  3. 处理请求 header 参数
  4. 处理响应数据
  5. 处理响应 header
  6. 处理响应 data

# 处理请求 url 参数

需求分析

axios({
  method: "get",
  url: "/base/get",
  params: {
    a: 1,
    b: 2
  }
});

我们希望最终请求的 url/base/get?a=1&b=2,这样服务端就可以通过请求的 url 解析到我们传来的参数数据了。实际上就是把 params 对象的 key 和 value 拼接到 url 上。

再来看几个更复杂的例子。

参数值为数组

axios({
  method: "get",
  url: "/base/get",
  params: {
    foo: ["bar", "baz"]
  }
});

最终请求的 url/base/get?foo[]=bar&foo[]=baz'

参数值为对象

axios({
  method: "get",
  url: "/base/get",
  params: {
    foo: {
      bar: "baz"
    }
  }
});

最终请求的 url/base/get?foo=%7B%22bar%22:%22baz%22%7Dfoo 后面拼接的是 {"bar":"baz"} encode 后的结果。

参数值为 Date 类型

const date = new Date();

axios({
  method: "get",
  url: "/base/get",
  params: {
    date
  }
});

最终请求的 url/base/get?date=2019-04-01T05:55:39.030Zdate 后面拼接的是 date.toISOString() 的结果。

特殊字符支持

对于字符 @:$,、``、[],我们是允许出现在 url 中的,不希望被 encode。

axios({
  method: "get",
  url: "/base/get",
  params: {
    foo: "@:$, "
  }
});

最终请求的 url/base/get?foo=@:$+,注意,我们会把空格 ``转换成+

空值忽略

对于值为 null 或者 undefined 的属性,我们是不会添加到 url 参数中的。

axios({
  method: "get",
  url: "/base/get",
  params: {
    foo: "bar",
    baz: null
  }
});

最终请求的 url/base/get?foo=bar

丢弃 url 中的哈希标记

axios({
  method: "get",
  url: "/base/get#hash",
  params: {
    foo: "bar"
  }
});

最终请求的 url/base/get?foo=bar

保留 url 中已存在的参数

axios({
  method: "get",
  url: "/base/get?foo=bar",
  params: {
    bar: "baz"
  }
});

最终请求的 url/base/get?foo=bar&bar=baz

具体实现

utils 下新建 url.ts

import { isDate, isObject } from ".";

function encode(val: string): string {
  return encodeURIComponent(val)
    .replace(/%40/g, "@")
    .replace(/%3A/gi, ":")
    .replace(/%24/g, "$")
    .replace(/%2C/gi, ",")
    .replace(/%20/g, "+")
    .replace(/%5B/gi, "[")
    .replace(/%5D/gi, "]");
}

export function buildURL(url: string, params?: any) {
  // 判断是否存在params
  if (!params) {
    return url;
  }
  // 最终存放的key-value数组
  const parts: string[] = [];

  Object.keys(params).forEach(key => {
    let val = params[key];
    // 排除val为null或者undefined
    if (val === null || typeof val === "undefined") {
      return;
    }
    // 临时存储values数组
    let values: string[];
    // 如果val是数组,key拼接[]字符串
    if (Array.isArray(val)) {
      values = val;
      key += "[]";
    } else {
      // 强行塞进数组
      values = [val];
    }
    // 判断是否是事件或者对象格式,分别处理
    values.forEach(val => {
      if (isDate(val)) {
        val = val.toISOString();
      } else if (isObject(val)) {
        val = JSON.stringify(val);
      }
      // 最后处理特殊字符,空格等等
      parts.push(`${encode(key)}=${encode(val)}`);
    });
  });

  // &分割数组
  let serializedParams = parts.join("&");
  if (serializedParams) {
    // 判断是否存在哈希,存在直接干掉哈希
    const hashIndex = url.indexOf("#");
    if (hashIndex !== -1) {
      serializedParams = serializedParams.slice(0, hashIndex);
    }
    // 判断url是否已经存在?
    url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams;
  }
  return url;
}

接着只需要在 app.ts 处模拟请求即可

# 处理请求 data 数据

上面处理 url 会存在 bug,如果传入对象格式的data,接口返回的 response会是空值。这里需要对 config.data 判断处理

export function transformRequest(data: any) {
  if (isObject(data)) {
    return JSON.stringify(data)
  }
  return data
}

//index.ts
function processConfig(config: AxiosRequestConfig) {
  ...
  config.data = transformBody(config)
}
// 转换data
function transformBody(config: AxiosRequestConfig) {
  const { data } = config
  return transformRequest(data)
}

# 处理请求 headers

做了请求数据的处理,把 data 转换成了 JSON 字符串,但是数据发送到服务端的时候,服务端并不能正常解析我们发送的数据。这是因为还需要改造请求头 Content-Type

// header.ts
function normalizeHearderName(headers: any, normalizedName: string): void {
  if (!headers) {
    return;
  }
  Object.keys(headers).forEach(name => {
    if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
      headers[normalizedName] = headers[name];
      // 删除多余项
      delete headers[name];
    }
  });
}
export function processHeaders(headers: any, data: any): any {
  // 处理大小写问题
  normalizeHearderName(headers, "Content-Type");
  // 如果data是纯对象格式,则需要添加content-type头
  if (isObject(data)) {
    if (headers && !headers["Content-Type"]) {
      headers["Content-Type"] = "application/json;charset=utf-8";
    }
  }
  return headers;
}

index.tsxhr.ts

// xhr.ts加入
Object.keys(headers).forEach(name => {
  // 当data为null,header就不需要存在了
  if (data === null && name.toLowerCase() === "content-type") {
    delete headers[name];
  } else {
    XML.setRequestHeader(name, headers[name]);
  }
});

// index.ts
function processConfig(config: AxiosRequestConfig) {
  config.url = transformUrl(config);
  // 此处必定是先处理headers 然后处理data,因为data为对象时需要设置headers的content-type
  config.headers = transformHeaders(config);
  config.data = transformBody(config);
}

// 转换headers
function transformHeaders(config: AxiosRequestConfig) {
  // 此处需要传入默认值
  const { headers = {}, data } = config;
  return processHeaders(headers, data);
}

至此,基础请求部分封装完成

# 处理响应数据

我们发送的请求都可以从网络层面接收到服务端返回的数据,但是代码层面并没有做任何关于返回数据的处理。我们希望能处理服务端响应的数据,并支持 Promise 链式调用的方式

axios({
  method: "post",
  url: "/base/post",
  data: {
    a: 1,
    b: 2
  }
}).then(res => {
  console.log(res);
});

接口定义

根据需求,我们可以定义一个 AxiosResponse 接口类型,如下:

export interface AxiosResponse {
  data: any;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request: any;
}

另外,axios 函数返回的是一个 Promise 对象,我们可以定义一个 AxiosPromise 接口,它继承于 Promise<AxiosResponse> 这个泛型接口:

export interface AxiosPromise extends Promise<AxiosResponse> {}

这样的话,当 axios 返回的是 AxiosPromise 类型,那么 resolve 函数中的参数就是一个 AxiosResponse 类型。

对于一个 AJAX 请求的 response,我们是可以指定它的响应的数据类型的,通过设置 XMLHttpRequest 对象的 responseType (opens new window) 属性,于是我们可以给 AxiosRequestConfig 类型添加一个可选属性:

export interface AxiosRequestConfig {
  // ...
  responseType?: XMLHttpRequestResponseType;
}

responseType 的类型是一个 XMLHttpRequestResponseType 类型,它的定义是 "" | "arraybuffer" | "blob" | "document" | "json" | "text" 字符串字面量类型。

实现获取响应数据逻辑

首先我们要在 xhr 函数添加 onreadystatechange (opens new window) 事件处理函数,并且让 xhr 函数返回的是 AxiosPromise 类型。

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise(resolve => {
    const { data, method = "get", url, headers, responseType } = config;
    const XML = new XMLHttpRequest();
    // 如果设置返回类型,则更新
    if (responseType) {
      XML.responseType = responseType;
    }
    XML.open(method.toUpperCase(), url, true);

    // 监听请求状态
    XML.onreadystatechange = function handleLoad() {
      if (XML.readyState !== 4) {
        return;
      }

      // 组装resolve的数据
      const responseHeaders = parseHeaders(XML.getAllResponseHeaders());
      // 返回数据 判断是否是否是文本
      const responseData = responseType && responseType !== "text" ? XML.response : XML.responseText;
      console.log(responseType);
      const response: AxiosResponse = {
        data: responseData,
        config,
        headers: responseHeaders,
        status: XML.status,
        request: XML,
        statusText: XML.statusText
      };
      resolve(response);
    };

    // 处理headers,真正添加请求头
    Object.keys(headers).forEach(name => {
      // 当data为null,header就不需要存在了
      if (data === null && name.toLowerCase() === "content-type") {
        delete headers[name];
      } else {
        XML.setRequestHeader(name, headers[name]);
      }
    });

    XML.send(data);
  });
}

处理响应 header

我们通过 XMLHttpRequest 对象的 getAllResponseHeaders 方法获取到的值是如下一段字符串:

date: Fri, 05 Apr 2019 12:40:49 GMT
etag: W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k"
connection: keep-alive
x-powered-by: Express
content-length: 13
content-type: application/json; charset=utf-8

接下来处理这段文本即可

export function parseHeaders(headers: string): any {
  let parsed = Object.create(null);
  // 判空
  if (!headers) {
    return parsed;
  }
  headers.split("\r\n").forEach(line => {
    let [key, value] = line.split(":");
    // 去空格转小写
    key = key.trim().toLowerCase();

    if (!key) {
      return;
    }
    if (value) {
      value = value.trim();
    }
    parsed[key] = value;
  });
  return parsed;
}

处理响应 data

我们希望默认不传入responseType 也将 string 类型的数据转换为对象格式

export function transformResponse(data: any): any {
  if (typeof data === "string") {
    try {
      data = JSON.parse(data);
    } catch (error) {
      console.log(error);
    }
  }
  return data;
}

至此基础需求封装已经实现,接下来处理其他部分。

基础部分代码:传送门 (opens new window)

# ts-axios 异常处理

首先需要实现以下异常捕获

axios({
  method: "get",
  url: "/error/get"
})
  .then(res => {
    console.log(res);
  })
  .catch(e => {
    console.log(e);
  });

错误处理可以分为三种:

  • 网络错误
  • 超时错误
  • 非 200 状态码错误

而这三部分均可以在 xhr.ts 中通过监听进行处理

// 监听请求状态
XML.onreadystatechange = function handleLoad() {
  if (XML.readyState !== 4) {
    return;
  }
  // 网络错误和超时错误时 status都为0
  if (XML.status === 0) {
    return;
  }

  // 组装resolve的数据
  const responseHeaders = parseHeaders(XML.getAllResponseHeaders());
  // 返回数据 判断是否是否是文本
  const responseData = responseType && responseType !== "text" ? XML.response : XML.responseText;
  const response: AxiosResponse = {
    data: responseData,
    config,
    headers: responseHeaders,
    status: XML.status,
    request: XML,
    statusText: XML.statusText
  };
  handleResponse(response);
};

// 网络异常
XML.onerror = function onError() {
  reject(createError("Network Error", config, null, XML));
};
// 超时错误
if (timeout) {
  XML.timeout = timeout;
}
XML.ontimeout = function onTimeOut() {
  reject(createError(`Timeout of ${timeout} ms exceeded`, config, "10000", XML));
};
// 处理非200状态码
function handleResponse(response: AxiosResponse) {
  if (response.status >= 200 && response.status < 300) {
    resolve(response);
  } else {
    reject(createError(`Request failed with status code ${response.status}`, config, null, XML, response));
  }
}

我们希望对外提供的信息不仅仅包含错误文本信息,还包括了请求对象配置 config,错误代码 codeXMLHttpRequest 对象实例 request以及自定义响应对象 response

axios({
  method: "get",
  url: "/error/timeout",
  timeout: 2000
})
  .then(res => {
    console.log(res);
  })
  .catch((e: AxiosError) => {
    console.log(e.message);
    console.log(e.request);
    console.log(e.code);
  });

首先创建 AxiosError 类,用于外部使用

export interface AxiosError extends Error {
  config: AxiosRequestConfig;
  code?: string;
  request?: any;
  response?: AxiosResponse;
  isAxiosError: boolean;
}

接着我们创建 error.ts 文件,然后实现 AxiosError 类,它是继承于 Error 类。

helpers/error.ts

import { AxiosRequestConfig, AxiosResponse } from "../types";

export class AxiosError extends Error {
  isAxiosError: boolean;
  config: AxiosRequestConfig;
  code?: string | null;
  request?: any;
  response?: AxiosResponse;

  constructor(
    message: string,
    config: AxiosRequestConfig,
    code?: string | null,
    request?: any,
    response?: AxiosResponse
  ) {
    super(message);

    this.config = config;
    this.code = code;
    this.request = request;
    this.response = response;
    this.isAxiosError = true;

    Object.setPrototypeOf(this, AxiosError.prototype);
  }
}

export function createError(
  message: string,
  config: AxiosRequestConfig,
  code?: string | null,
  request?: any,
  response?: AxiosResponse
): AxiosError {
  const error = new AxiosError(message, config, code, request, response);

  return error;
}

AxiosError 继承于 Error 类,添加了一些自己的属性:configcoderequestresponseisAxiosError 等属性。这里要注意一点,我们使用了 Object.setPrototypeOf(this, AxiosError.prototype),这段代码的目的是为了解决 TypeScript 继承一些内置对象的时候的坑,参考 (opens new window)

另外,为了方便使用,我们对外暴露了一个 createError 的工厂方法。

修改关于错误对象创建部分的逻辑,如下:

xhr.ts

import { createError } from "./helpers/error";

request.onerror = function handleError() {
  reject(createError("Network Error", config, null, request));
};

request.ontimeout = function handleTimeout() {
  reject(createError(`Timeout of ${config.timeout} ms exceeded`, config, "ECONNABORTED", request));
};

function handleResponse(response: AxiosResponse) {
  if (response.status >= 200 && response.status < 300) {
    resolve(response);
  } else {
    reject(createError(`Request failed with status code ${response.status}`, config, null, request, response));
  }
}

在 demo 中,TypeScript 并不能把 e 参数推断为 AxiosError 类型,于是我们需要手动指明类型,为了让外部应用能引入 AxiosError 类型,我们也需要把它们导出。

我们创建 axios.ts 文件,把之前的 index.ts 的代码拷贝过去,然后修改 index.ts 的代码。

index.ts

import axios from "./axios";

export * from "./types";

export default axios;

这样我们在 demo 中就可以引入 AxiosError 类型了。

examples/error/app.ts

import axios, { AxiosError } from "../../src/index";

axios({
  method: "get",
  url: "/error/timeout",
  timeout: 2000
})
  .then(res => {
    console.log(res);
  })
  .catch((e: AxiosError) => {
    console.log(e.message);
    console.log(e.code);
  });

# ts-axios 接口扩展

需求分析

为了用户更加方便地使用 axios 发送请求,我们可以为所有支持请求方法扩展一些接口:

  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])

如果使用了这些方法,我们就不必在 config 中指定 urlmethoddata 这些属性了。

从需求上来看,axios 不再单单是一个方法,更像是一个混合对象,本身是一个方法,又有很多方法属性,接下来我们就来实现这个混合对象。

接口定义

根据需求分析,混合对象 axios 本身是一个函数,我们再实现一个包括它属性方法的类,然后把这个类的原型属性和自身属性再拷贝到 axios 上。

我们先来给 axios 混合对象定义接口:

types/index.ts

export interface Axios {
  request(config: AxiosRequestConfig): AxiosPromise;

  get(url: string, config?: AxiosRequestConfig): AxiosPromise;

  delete(url: string, config?: AxiosRequestConfig): AxiosPromise;

  head(url: string, config?: AxiosRequestConfig): AxiosPromise;

  options(url: string, config?: AxiosRequestConfig): AxiosPromise;

  post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise;

  put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise;

  patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise;
}

export interface AxiosInstance extends Axios {
  (config: AxiosRequestConfig): AxiosPromise;
}

export interface AxiosRequestConfig {
  url?: string;
  // ...
}

首先定义一个 Axios 类型接口,它描述了 Axios 类中的公共方法,接着定义了 AxiosInstance 接口继承 Axios,它就是一个混合类型的接口。

另外 AxiosRequestConfig 类型接口中的 url 属性变成了可选属性。

创建 axios 类

我们创建一个 Axios 类,来实现接口定义的公共方法。我们创建了一个 core 目录,用来存放发送请求核心流程的代码。我们在 core 目录下创建 Axios.ts 文件。

core / Axios.ts;
import { AxiosRequestConfig, AxiosPromise, Method } from "../types";
import dispatchRequest from "./dispatchRequest";

export default class Axios {
  request(config: AxiosRequestConfig): AxiosPromise {
    return dispatchRequest(config);
  }

  get(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData("get", url, config);
  }

  delete(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData("delete", url, config);
  }

  head(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData("head", url, config);
  }

  options(url: string, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithoutData("options", url, config);
  }

  post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithData("post", url, data, config);
  }

  put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithData("put", url, data, config);
  }

  patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
    return this._requestMethodWithData("patch", url, data, config);
  }

  _requestMethodWithoutData(method: Method, url: string, config?: AxiosRequestConfig) {
    return this.request(
      Object.assign(config || {}, {
        method,
        url
      })
    );
  }

  _requestMethodWithData(method: Method, url: string, data?: any, config?: AxiosRequestConfig) {
    return this.request(
      Object.assign(config || {}, {
        method,
        url,
        data
      })
    );
  }
}

其中 request 方法的功能和我们之前的 axios 函数功能是一致。axios 函数的功能就是发送请求,基于模块化编程的思想,我们把这部分功能抽出一个单独的模块,在 core 目录下创建 dispatchRequest 方法,把之前 axios.ts 的相关代码拷贝过去。另外我们把 xhr.ts 文件也迁移到 core 目录下。

core/dispatchRequest.ts

import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "../types";
import xhr from "./xhr";
import { buildURL } from "../helpers/url";
import { transformRequest, transformResponse } from "../helpers/data";
import { processHeaders } from "../helpers/headers";

export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
  processConfig(config);
  return xhr(config).then(res => {
    return transformResponseData(res);
  });
}

function processConfig(config: AxiosRequestConfig): void {
  config.url = transformURL(config);
  config.headers = transformHeaders(config);
  config.data = transformRequestData(config);
}

function transformURL(config: AxiosRequestConfig): string {
  const { url, params } = config;
  return buildURL(url, params);
}

function transformRequestData(config: AxiosRequestConfig): any {
  return transformRequest(config.data);
}

function transformHeaders(config: AxiosRequestConfig) {
  const { headers = {}, data } = config;
  return processHeaders(headers, data);
}

function transformResponseData(res: AxiosResponse): AxiosResponse {
  res.data = transformResponse(res.data);
  return res;
}

回到 Axios.ts 文件,对于 getdeleteheadoptionspostpatchput 这些方法,都是对外提供的语法糖,内部都是通过调用 request 方法实现发送请求,只不过在调用之前对 config 做了一层合并处理。

混合对象实现

混合对象实现思路很简单,首先这个对象是一个函数,其次这个对象要包括 Axios 类的所有原型属性和实例属性,我们首先来实现一个辅助函数 extend

helpers/util.ts
export function extend<T, U>(to: T, from: U): T & U {
  for (const key in from) {
    ;(to as T & U)[key] = from[key] as any
  }
  return to as T & U
}

extend 方法的实现用到了交叉类型,并且用到了类型断言。extend 的最终目的是把 from 里的属性都扩展到 to 中,包括原型上的属性。

我们接下来对 axios.ts 文件做修改,我们用工厂模式去创建一个 axios 混合对象。

axios.ts

import { AxiosInstance } from "./types";
import Axios from "./core/Axios";
import { extend } from "./helpers/util";

function createInstance(): AxiosInstance {
  const context = new Axios();
  const instance = Axios.prototype.request.bind(context);

  extend(instance, context);

  return instance as AxiosInstance;
}

const axios = createInstance();

export default axios;

createInstance 工厂函数的内部,我们首先实例化了 Axios 实例 context,接着创建instance 指向 Axios.prototype.request 方法,并绑定了上下文 context;接着通过 extend 方法把 context 中的原型方法和实例方法全部拷贝到 instance 上,这样就实现了一个混合对象:instance 本身是一个函数,又拥有了 Axios 类的所有原型和实例属性,最终把这个 instance 返回。由于这里 TypeScript 不能正确推断 instance 的类型,我们把它断言成 AxiosInstance 类型。

这样我们就可以通过 createInstance 工厂函数创建了 axios,当直接调用 axios 方法就相当于执行了 Axios 类的 request 方法发送请求,当然我们也可以调用 axios.getaxios.post 等方法。

测试

examples 目录下创建 extend 目录,在 extend 目录下创建 index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Extend example</title>
  </head>
  <body>
    <script src="/__build__/extend.js"></script>
  </body>
</html>

接着创建 app.ts 作为入口文件:

import axios from "../../src/index";

axios({
  url: "/extend/post",
  method: "post",
  data: {
    msg: "hi"
  }
});

axios.request({
  url: "/extend/post",
  method: "post",
  data: {
    msg: "hello"
  }
});

axios.get("/extend/get");

axios.options("/extend/options");

axios.delete("/extend/delete");

axios.head("/extend/head");

axios.post("/extend/post", { msg: "post" });

axios.put("/extend/put", { msg: "put" });

axios.patch("/extend/patch", { msg: "patch" });

# ts-axios 拦截器

暂定

# ts-axios 配置化,取消

暂定

# 扩展功能实现

暂定

# 单元测试

单元测试是前端一个很重要的方向,鉴别一个开源库是否靠谱的一个标准是它的单元测试是否完善。有了完整的单元测试,未来你去重构现有代码或者是增加新的需求都会有十足的把握不出现 regression bug。

# Jest 安装和配置

# 辅助模块单元测试

# 请求模块单元测试

# 编译和发布

  1. 官网 (opens new window)注册账号

  2. 本地登录

    npm login
    username:
    password:
    email:
    
  3. 修改打包的文件名,和主入口文件,例如rollup.config.ts

const libraryName = "axios";

export default {
  input: `src/index.ts`,
  output: [
    { file: pkg.main, name: camelCase(libraryName), format: "umd", sourcemap: true },
    { file: pkg.module, format: "es", sourcemap: true }
  ],
  // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
  external: [],
  watch: {
    include: "src/**"
  },
  plugins: [
    // Allow json resolution
    json(),
    // Compile TypeScript files
    typescript({ useTsconfigDeclarationDir: true }),
    // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
    commonjs(),
    // Allow node_modules resolution, so you can use 'external' to control
    // which external modules to include in the bundle
    // https://github.com/rollup/rollup-plugin-node-resolve#usage
    resolve(),

    // Resolve source maps to the original source
    sourceMaps()
  ]
};
  1. 修改 package.json 主入口mainmodule 以及typings

    {
      "main": "dist/axios.umd.js",
      "module": "dist/axios.es5.js",
      "typings": "dist/types/index.d.ts",
      "files": ["dist"]
    }
    
  2. 修改仓库信息

  3. 添加打包命令

    {
      "prepublish": "npm run test:prod && npm run build",
      "publish": "sh release.sh"
    }
    
  4. 添加打包脚本

    #!/usr/bin/env sh
    set -e
    echo "Enter release version: "
    read VERSION
    read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r
    echo  # (optional) move to a new line
    if [[ $REPLY =~ ^[Yy]$ ]]
    then
      echo "Releasing $VERSION ..."
    
      # commit
      git add -A
      git commit -m "[build] $VERSION"
      npm version $VERSION --message "[release] $VERSION"
      git push origin master
    
      # publish
      npm publish
    fi
    

    #!/usr/bin/env sh 用来表示它是一个 shell 脚本。

    set -e 告诉脚本如果执行结果不为 true 则退出。

    echo "Enter release version: " 在控制台输出 Enter release version:

    read VERSION 表示从标准输入读取值,并赋值给 $VERSION 变量。

    read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r,其中 read -p 表示给出提示符,后面接着 Releasing $VERSION - are you sure? (y/n) 提示符;-n 1 表示限定最多可以有 1 个字符可以作为有效读入;-r 表示禁止反斜线的转义功能。因为我们的 read 并没有指定变量名,那么默认这个输入读取值会赋值给 $REPLY 变量。

    echo 输出空值表示跳到一个新行,# 在 shell 脚本中表示注释。

    if [[ $REPLY =~ ^[Yy]$ ]] 表示 shell 脚本中的流程控制语句,判断 $REPLY 是不是大小写的 y,如果满足,则走到后面的 then 逻辑。

    echo "Releasing $VERSION ..." 在控制台输出 Releasing $VERSION ...

    git add -A 表示把代码所有变化提交到暂存区。

    git commit -m "[build] $VERSION" 表示提交代码,提交注释是 [build] $VERSION

    npm version $VERSION --message "[release] $VERSION" 是修改 package.json 中的 version 字段到 $VERSION,并且提交一条修改记录,提交注释是 [release] $VERSION

    git push origin master 是把代码发布到主干分支。

    npm publish 是把仓库发布到 npm 上,我们会把 dist 目录下的代码都发布到 npm 上,因为我们在 package.json 中配置的是 files["dist"]

Last update: 7/20/2021, 8:53:54 AM