Axios源码分析

Axios是一个基于promise的HTTP库,可以用在浏览器和node.js中。

文档地址:https://github.com/axios/axios

axios理解和使用

1.请求配置

  1  {
  2      // 请求服务器的URL
  3      url: '/user',
  4      
  5      // method 创建请求使用的方法
  6      method: 'get'
  7      
  8      // baseURL 将自动加早url前面,除非 url 是一个绝对url
  9      baseURL: 'https://some-domain.com/api/'
 10      
 11      // 'transformRequest' 允许向服务器发送前,修改请求数据
 12      // 只能用在 PUT, POST 和 PATH 这几个请求方法
 13      // 后面数组中的函数必须返回一个字符串,或ArrayBuffer,或 Stream
 14      transformRequest: [function(data, headers) {
 15          // 对 data 进行任意转换处理
 16          return data;
 17      }]
 18      
 19      //  'transformResponse' 在传递给 then/catch前, 允许修改响应数据
 20      transformResponse:[function(data) {
 21          // 对 data 进行任意转换处理
 22          return data
 23      }]
 24      
 25      // 'headers' 是即将被发送的自定义请求头
 26      headers: {  'X-Requested-With': 'XMLHttpRequest' }
 27      
 28      // 'params' 是即将与请求一起发送的URL参数
 29      // 必须是一个无格式对象(plain object) 或 URLSearchParams对象
 30      params: {
 31          ID: 12345
 32      }
 33      
 34      // 'paramsSerializer'是一个负责 'params' 序列化的函数
 35      paramsSerializer: function(params) {
 36          return Qs.stringify(params, {arrayFormat: 'brackets'})
 37      }
 38      
 39      // 'data' 是作为请求主体被发送的数据
 40      // 只适用于这些请求方法 PUT POST PATHCH
 41      // 在没有设置 `transformRequest` 时, 必须以下类型之一:
 42      // - string,plain object, ArrayBuffer,ArrayBufferView,URLSearchParams
 43      // - 浏览器专属: FormData, File, Blob
 44      // - Node 专属: Stream
 45      data: {
 46          firstName: 'Fred'
 47      }
 48      
 49      // 指定请求超时的毫秒数(0表示无超时时间)
 50      // 请求超时,请求将被中断
 51      timeout: 1000
 52      
 53      // 'withCreadentials' 表示跨越请求时是否需要使用凭证
 54      withCreadentials: true, // default -> false
 55      
 56      // 'adapter' 允许自定义处理请求,以使测试更轻松
 57      adapter: function(config) {
 58          /* */
 59      }    
 60      
 61      // 'auth' 表示应该使用 HTTP 基础验证,并提供凭证
 62      // 这将设置一个'Authorization' 头,覆写掉现有的任意使用'hedaers'设置的自定义'Authorization'
 63      auth: {
 64          username: 'janedoe',
 65          password: 's00pers3cret'
 66      }
 67      
 68      //  'responseType' 表示服务器响应的数据类型, 可以是 'arraybuffer', 'blob', 'document', 'json'
 69      responseType: 'json',
 70      
 71      // 'xsrfCookieName' 是作用于xsrf token 的值得cookie的名称
 72      xsrfCookieName: 'XSRF-TOKEN'
 73      
 74      // 'onUploadProgress' 允许为上传处理进度事件
 75      onUploadProgress: function(progressEvent) {
 76          // Do whatever you want with the native progress event
 77      }
 78      
 79      // 'onDownloadProgress' 允许为下载处理进度事件
 80      onDownloadProgress: function(progressEvent) {
 81          // 对原生进度事件的处理
 82      }
 83      
 84      // 'maxContentLength' 定义允许的响应内容的最大尺寸
 85      maxContentLength: 2000
 86      
 87      // 'validateStatus' 定义对于给定的HTTP响应状态码是resolve 或 reject promis
 88      validateStatus: function(status) {
 89          return status >=200 && status <300
 90      }
 91      
 92      // 'maxRedirects' 定义在node.js中 follow的最大重定向数目
 93      // 如果设置为0, 将不会 follow 任何重定向
 94      maxRedirects: 5, // default
 95      
 96      // 'proxy' 定义代理服务器的主机名称和端口
 97      // 'auth' 表示HTTP 基础验证应当用于连接代理,并提供凭证
 98      // 这将会设置一个 'Proxy-Authorization'头,覆盖掉已有的通过使用`header`设置的自定义
 99      proxy: {
100          host: '127.0.01',
101          port: 9000,
102          auth: {
103              username: 'mikeymike',
104              password: 'rapunz3l'
105          }
106      }
107      
108      // 'cancelToken' 指定用于取消请求的 cancel token
109      cancelToken: new CancelToken(function(cancel) {
110          
111      })
112  }

 

2.响应结构

某个请求的响应包含以下信息

 
 1 {
 2      // 'data' 由服务器提供的响应
 3      data: {}
 4      
 5      // 'status' 来自服务器响应的 HTTP 状态信息
 6      status: 200,
 7          
 8       // 'statusText' 来自服务器响应的HTTP 状态信息
 9       statusText: 'OK'
10      
11      // 'headers' 服务器响应的头
12      headers: {}
13      
14      // 'config' 是为请求提供的配置信息
15      config: {}
16      
17      request: {}
18  }

 

 

3.axios特点

1.基于promise的异步ajax请求库。

2.浏览器端/ node端都可以使用。

3.支持请求/ 响应拦截器。

4.支持取消请求。

5.请求/ 响应数据转换。

6.批量发送多个请求。

 

4.axios.create(config)

1.根据指定配置创建一个新的axios,也就是每个新axios都有自己的配置

2.新axios只是没有取消请求和批量发请求的方法,其他所有语法都是一致的

3.为什么要设计这个语法?

(1) 需要:项目中有部分按接口需要的配置与另一部分接口需求的配置不太一样,如何处理?

(2) 解决:创建2个新axios,每个人都有特有的配置,分别应用到不同要求的接口请求中

 

5.axios的处理链流程

 
 1 // 添加请求拦截器(回调函数) -> 后添加先执行
 2  axios.interceptors.request.use(
 3      config => {
 4          return config
 5      },
 6      error => {
 7          return Promise.reject(error)
 8      }
 9  )
10 11  // 添加响应拦截器
12  axios.interceptors.response.use(
13      response => {
14          return response
15      },
16      error => {
17          return Promise.reject(error)
18      }
19  )

 

6.取消请求

 1  let cancel  // 用于保存取消请求的函数
 2  getProducts() {
 3      // 准备发请求前,取消未完成的请求
 4      if( typeof cancel === 'function' ) {
 5          cancel('取消请求')
 6      }
 7      axios({
 8          url: 'http://localhost:8000/products1',
 9          cancelToken: new axios.CancelToken((c) => { // c是用于取消当前请求的函数
10          // 保存取消函数,用于之后可能需要取消当前请求
11          cancel = c;
12      })
13      }).then(
14          response => {
15              cancel = null
16              consoel.log('请求成功了')
17          },
18          error => {
19              if(axios.isCancel(error)) {
20                  console.log('取消请求的错误')
21              } else { // 请求出错
22                  cancel = null
23                  console.log(error.message)
24              }    
25          }
26      )
27  }
28 29  cancelReq() {
30      if(type cancel === 'function') {
31          cancel('强制取消请求')
32      } else {
33          console.log('没有可取消的请求')
34      }
35  }
36  // 调用
37  cancelReq()

 

 

axios源码阅读

1.文件目录

dist ->打包生成后的文件

examples -> 一些例子

lib -> 核心文件代码

adapters -> 请求相关的文件夹

http.js -> node服务端发送http请求

xhr.js -> 真正发请求的模块

Cancel -> 取消请求相关的文件夹

Cancel.js -> 定义的是Cancel构造函数

CancelToken.js -> 定义的是一个构造函数,执行取消相关的

isCancel.js -> 判断一个error是不是cancel类型错误

core -> 核心的一些东西

Axios.js -> Axios构造函数

dispathRequest.js -> 分发请求

InterceptorManager.js -> 拦截器相关

helpers ->工具模块

axios.js -> 向外暴露axios函数

defaults.js -> 默认配置相关的东西

index.js -> 外层的入口文件

 

2.axios与Axios的关系

①、从语法上来说:axios不是Axios的实例。

②、从功能上来说:axios是Axios的实例。(1.有自身的属性。2.有他原型链上的方法)

③、axios是Axios.prototype函数bind()返回的函数

④、axios作为对象有Axios原型对象上的所有方法,有Axios对象上所有属性

源码中axios.js文件

 1  /**
 2   * Create an instance of Axios
 3   *
 4   * @param {Object} defaultConfig The default config for the instance
 5   * @return {Axios} A new instance of Axios
 6   */
 7  function createInstance(defaultConfig) {
 8    var context = new Axios(defaultConfig);
 9    // 等同于 Axios.prototype.request.bind(context)
10    // bind返回一个新函数,这个新函数内部会调用这个request函数,
11    // 简单的理解:axios函数最终找request执行
12    // context是Axios的实例,一旦使用了request函数里面的this指向的就是Axios实例
13    var instance = bind(Axios.prototype.request, context); // axios
14 15    // 将Axios原型对象上的方法拷贝到 instance上:request()/get()/post()/put()/delete()
16    utils.extend(instance, Axios.prototype, context);
17 18    // Copy context to instance
19    //  将Axios实例对象上的属性拷贝到instance上:defaults和interceptors属性
20    utils.extend(instance, context);
21 22    return instance;
23  }
24 25  // Create the default instance to be exported
26  var axios = createInstance(defaults);

 


 

 

3.instance与axios的区别?

1.相同:

(1)都是一个能发任意请求的函数: request(config)

(2)都有发特定请求的各种方法:get()/post()/put()/delete()

(3)都有默认配置和拦截器属性:defaults/interceptors

2.不同:

(1)默认匹配的值很可能不一样

(2)instance没有axios后面添加的一些方法:create()/CancelToken()/all()

源码中axios.js文件

 1  // Factory for creating new instances
 2  axios.create = function create(instanceConfig) {
 3    // 还是调用了createInstance方法,跟上面一样再走里面一遍
 4    return createInstance(mergeConfig(axios.defaults, instanceConfig));
 5  };
 6  7  // Expose Cancel & CancelToken
 8  // 不一样的地方是axios添加了这些,而axios.create并没有添加这些操作
 9  axios.Cancel = require('./cancel/Cancel');
10  axios.CancelToken = require('./cancel/CancelToken');
11  axios.isCancel = require('./cancel/isCancel');
12 13  // Expose all/spread
14  axios.all = function all(promises) {
15    return Promise.all(promises);
16  };

 

4.axios运行的整体流程?

主要流程:

request(config) == > dispatchRequest(config) ===> xhrAdapter(config)

request(config):

将请求拦截器 / dispatchRequest() / 响应拦截器 通过promise链串起来,然后promise

dispatchRequest(config) :

转换请求数据 ===> 调用xhrAdapter()发请求 ===> 请求返回后转换响应数据.返回promise

xhrAdapter(config):

创建XHR对象,根据config进行相应设置,发送特定请求,并接收响应数据,返回promise。

 

axiso流程图:

request(config)

源码axios.js / Axios.js文件

Promise通过它的链使用将请求拦截器,发请求的操作,响应拦截器,以及我们的最后请求的成功失败串联起来

 
 1 /**
 2   * Create an instance of Axios
 3   *
 4   * @param {Object} defaultConfig The default config for the instance
 5   * @return {Axios} A new instance of Axios
 6   */
 7  function createInstance(defaultConfig) {
 8    var context = new Axios(defaultConfig);
 9    // Axios.prototype.request.bind(context)
10    var instance = bind(Axios.prototype.request, context); // axios
11 12    // Copy axios.prototype to instance
13    // 将Axios原型对象上的方法拷贝到 instance上:request()/get()/post()/put()/delete()
14    utils.extend(instance, Axios.prototype, context);
15 16    // Copy context to instance
17    //  将Axios实例对象上的属性拷贝到instance上:defaults和interceptors属性
18    utils.extend(instance, context);
19 20    return instance;
21  }
22 23  // Create the default instance to be exported
24  var axios = createInstance(defaults);
25 26  // Expose Axios class to allow class inheritance
27  axios.Axios = Axios;
28 29  // Factory for creating new instances
30  axios.create = function create(instanceConfig) {
31    return createInstance(mergeConfig(axios.defaults, instanceConfig));
32  };

 


 

 

 
 1 /**
 2   * 主要用于发请求的函数
 3   * 我们使用的axios就是此函数bind()返回的函数
 4   * 
 5   * Dispatch a request
 6   *
 7   * @param {Object} config The config specific for this request (merged with this.defaults)
 8   */
 9  Axios.prototype.request = function request(config) {
10    /*eslint no-param-reassign:0*/
11    // Allow for axios('example/url'[, config]) a la fetch API
12    if (typeof config === 'string') {
13      config = arguments[1] || {};
14      config.url = arguments[0];
15    } else {
16      config = config || {};
17    }
18 19    config = mergeConfig(this.defaults, config);
20 21    // Set config.method
22    if (config.method) {
23      config.method = config.method.toLowerCase();
24    } else if (this.defaults.method) {
25      config.method = this.defaults.method.toLowerCase();
26    } else {
27      config.method = 'get';
28    }
29 30    ------
31    // Promise通过它的链使用将请求拦截器,发请求的操作,响应拦截器,以及我们的最厚请求的成功失败串联起来
32 33    // Hook up interceptors middleware
34    // undefined作用:chain后面是两两一组进行调用,有了它作为发请求那一组的reject失败回调,保证了     响应拦截器还是被两两一组返回调用,避免错位
35    var chain = [dispatchRequest, undefined];
36    var promise = Promise.resolve(config);
37 38    // 找到所有的请求拦截器函数,后添加的请求拦截器保存在数组的前面
39    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
40      chain.unshift(interceptor.fulfilled, interceptor.rejected);
41    });
42 43    // 后添加的响应拦截器保存在数组后面
44    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
45      chain.push(interceptor.fulfilled, interceptor.rejected);
46    });
47 48    // 通过promise的then()串连起来所有的请求拦截器/请求方法/ 响应拦截器
49    while (chain.length) {
50      promise = promise.then(chain.shift(), chain.shift());
51    }
52 53    // 返回用来指定我们的onResolved和onRejected的promise
54    return promise;
55  }; 

 


 

 

dispatchRequest(config)

源码dispatchRequest.js/default.js文件

 
 1 /**
 2   * Dispatch a request to the server using the configured adapter.
 3   *
 4   * @param {object} config The config that is to be used for the request
 5   * @returns {Promise} The Promise to be fulfilled
 6   */
 7  module.exports = function dispatchRequest(config) {
 8    throwIfCancellationRequested(config);
 9 10    // Ensure headers exist
11    config.headers = config.headers || {};
12 13    // 对config中的data进行必要的处理转换
14    // 设置相应的Content-Type请求头
15    // Transform request data
16    config.data = transformData(
17      config.data,
18      config.headers,
19      // 转换数据格式
20      config.transformRequest // --对应在defalut文件中
21    );
22 23    // Flatten headers
24    // 整合config中所有的header
25    config.headers = utils.merge(
26      config.headers.common || {},
27      config.headers[config.method] || {},
28      config.headers
29    );
30 31    utils.forEach(
32      ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
33      function cleanHeaderConfig(method) {
34        delete config.headers[method];
35      }
36    );
37 38    var adapter = config.adapter || defaults.adapter;
39 40    return adapter(config).then(function onAdapterResolution(response) {
41      throwIfCancellationRequested(config);
42 43      // Transform response data
44      // 对response中还没有解析的data数据进行解析
45      // Json字符串解析为js对象/数组
46      response.data = transformData(
47        response.data,
48        response.headers,
49        // 转换数据格式
50        config.transformResponse // --对应在defalut文件中
51      );
52 53      return response;
54    }, function onAdapterRejection(reason) {
55      if (!isCancel(reason)) {
56        throwIfCancellationRequested(config);
57 58        // Transform response data
59        if (reason && reason.response) {
60          reason.response.data = transformData(
61            reason.response.data,
62            reason.response.headers,
63            config.transformResponse
64          );
65        }
66      }
67 68      return Promise.reject(reason);
69    });
70  };

 

 
 1 // 得到当前环境对应的请求适配器
 2    adapter: getDefaultAdapter(),
 3  4    // 请求转换器
 5    transformRequest: [function transformRequest(data, headers) {
 6      normalizeHeaderName(headers, 'Accept');
 7      normalizeHeaderName(headers, 'Content-Type');
 8      if (utils.isFormData(data) ||
 9        utils.isArrayBuffer(data) ||
10        utils.isBuffer(data) ||
11        utils.isStream(data) ||
12        utils.isFile(data) ||
13        utils.isBlob(data)
14      ) {
15        return data;
16      }
17      if (utils.isArrayBufferView(data)) {
18        return data.buffer;
19      }
20      if (utils.isURLSearchParams(data)) {
21        setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
22        return data.toString();
23      }
24      // 如果data是对象,指定请求体参数格式为json,并将参数数据对象转换为json
25      if (utils.isObject(data)) {
26        setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
27        return JSON.stringify(data);
28      }
29      return data;
30    }],
31 32    // 响应数据转换器:解析字符串类型的data数据
33    transformResponse: [function transformResponse(data) {
34      /*eslint no-param-reassign:0*/
35      if (typeof data === 'string') {
36        try {
37          data = JSON.parse(data);
38        } catch (e) { /* Ignore */ }
39      }
40      return data;
41    }],

 

xhrAdapter(config)

源码xhr.js文件

 
 1 // 创建XHR对象 
 2      var request = new XMLHttpRequest();
 3  4      // HTTP basic authentication
 5      if (config.auth) {
 6        var username = config.auth.username || '';
 7        var password = unescape(encodeURIComponent(config.auth.password)) || '';
 8        requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
 9      }
10 11      var fullPath = buildFullPath(config.baseURL, config.url);
12 13      // 初始化请求
14      request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
15      // buildURL在help文件夹buildURL.js处理请求url
16 17      // Set the request timeout in MS
18      // 指定超时的时间
19      request.timeout = config.timeout;
20 21      // Listen for ready state
22      // 绑定请求状态改变的监听
23      request.onreadystatechange = function handleLoad() {
24        if (!request || request.readyState !== 4) {
25          return;
26        }
27 28        // The request errored out and we didn't get a response, this will be
29        // handled by onerror instead
30        // With one exception: request that using file: protocol, most browsers
31        // will return status as 0 even though it's a successful request
32        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
33          return;
34        }
35 36        // Prepare the response
37        // 准备response对象
38        var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
39        var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
40        var response = {
41          data: responseData,
42          status: request.status,
43          statusText: request.statusText,
44          headers: responseHeaders,
45          config: config,
46          request: request
47        };
48 49        // 根据响应状态码来确定请求的promise的结果状态(成功/失败)
50        settle(resolve, reject, response); // 下方settle.js文件
51 52        // Clean up request
53        request = null;
54      };
55 56      // Handle browser request cancellation (as opposed to a manual cancellation)
57      // 绑定请求中断监听
58      request.onabort = function handleAbort() {
59        if (!request) {
60          return;
61        }
62 63        reject(createError('Request aborted', config, 'ECONNABORTED', request));
64 65        // Clean up request
66        request = null;
67      };
68 69      // Not all browsers support upload events
70      // 绑定上传进度的监听
71      if (typeof config.onUploadProgress === 'function' && request.upload) {
72        request.upload.addEventListener('progress', config.onUploadProgress);
73      }
74 75      // 如果配置了cancelToken
76      if (config.cancelToken) {
77        // Handle cancellation
78        // 指定用于中断请求的回调函数
79        config.cancelToken.promise.then(function onCanceled(cancel) {
80          if (!request) {
81            return;
82          }
83 84          // 中断请求
85          request.abort();
86          // 让请求的promise失败
87          reject(cancel);
88          // Clean up request
89          request = null;
90        });
91      }
92 93      if (!requestData) {
94        requestData = null;
95      }
96 97      // Send the request
98      // 发送请求,指定请求体数据,可能是null
99      request.send(requestData);

 

settle.js文件

 1  /**
 2   * Resolve or reject a Promise based on response status.
 3   *
 4   * @param {Function} resolve A function that resolves the promise.
 5   * @param {Function} reject A function that rejects the promise.
 6   * @param {object} response The response.
 7   */
 8  module.exports = function settle(resolve, reject, response) {
 9    var validateStatus = response.config.validateStatus;
10    if (!response.status || !validateStatus || validateStatus(response.status)) {
11       // 请求成功
12      resolve(response);
13    } else {
14       // 请求出错
15      reject(createError( // 源码createError.js内 创建error对象的函数
16        'Request failed with status code ' + response.status,
17        response.config,
18        null,
19        response.request,
20        response
21      ));
22    }
23  };
24 25  default.js文件
26 27  // 判读状态码的合法性: [200,299]
28    validateStatus: function validateStatus(status) {
29      return status >= 200 && status < 300;
30    }

 

 

5.axios的请求/响应数据转换器是什么?

函数

1.请求转换器:对请求头和请求体数据进行特定处理的函数

 
1 // 如果data是对象,指定请求体参数格式为json,并将参数数据对象转换为json
2      if (utils.isObject(data)) {
3        setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
4        return JSON.stringify(data);
5      }

 

2.响应转换器: 将响应体json字符串解析为js对象或数组的函数

 
1 if (typeof data === 'string') {
2        try {
3          data = JSON.parse(data);
4        } catch (e) { /* Ignore */ }
5      }

 

 

6.response的整体结构

 
1  {
2          data,
3          status,
4          statusText,
5          headers,
6          config,
7          request
8  };
9  // 对应源码adapters文件夹的xhr.js

 

 

7.error整体结构

1  {
2     config,
3     request,
4     response,
5     ...
6  }
7  // 对应源码core文件夹的enhanceError.js

 

 

8.如何取消未完成的请求?

1.当配置了cancelToken对象时,保存cancel函数

(1) 创建一个用于将来中断请求的cancelPromise

(2) 并定义了一个用于取消请求的cancel函数

(3)将cancel函数传递出来

2.调用cancel()取消请求

(1)执行cancel函数传入错误信息message

(2)内部会让cancelPromise变为成功,且成功的值为一个Cancel对象

(3)在cancelPromise的成功回调中中断请求,并让发请求的promise失败,失败的reason为Cancel对象

源码CancelToken.js / xhr.js

 
 1 function CancelToken(executor) {
 2    if (typeof executor !== 'function') {
 3      throw new TypeError('executor must be a function.');
 4    }
 5  6    // 为取消请求准备一个promise对象,并保存resolve函数到外部(外部有机会可以使用promise)
 7    var resolvePromise;
 8    this.promise = new Promise(function promiseExecutor(resolve) {
 9      resolvePromise = resolve;
10    });
11 12    // 保存当前token对象
13    var token = this;
14    // 执行器函数是外部定义,内部调用
15    // 立即执行接收的执行器函数,并传入用于取消请求的cancel函数
16    // cancel函数是内部定义,外部调用    -》结合上面(axios理解使用第6点一起康康)
17    executor(function cancel(message) {
18      // 如果token中有reason了,说明请求已取消。 | 这个if后看,先看下面的token.reason
19      if (token.reason) {
20        // Cancellation has already been requested
21        return;
22      }
23 24      // 将token的reason指定为一个Canel对象
25      token.reason = new Cancel(message);
26      // 将取消请求的promise指定为成功,值为reason
27      resolvePromise(token.reason);
28    });
29  }

 

 
// 如果配置了cancelToken
     if (config.cancelToken) {
       // Handle cancellation
       // 指定用于中断请求的回调函数
       config.cancelToken.promise.then(function onCanceled(cancel) {
         if (!request) {
           // 如果请求还没有结束,可以走后面的中断请求
           // 如果请求结束了,直接return,无法走后面的中断请求一系列操作
           return;
         }
 ​
         // 中断请求
         request.abort();
         // 让请求的promise失败
         reject(cancel);
         // Clean up request
         request = null;
       });
     }

 

 HTTP相关、XHR相关具体源码内部注释欢迎查看Github。
或者进入我的博客专栏查阅

 

 

 

posted @ 2020-08-05 19:11  小羽羽  阅读(810)  评论(0编辑  收藏  举报