pull/8/head
zjx0905 2020-07-27 11:40:32 +08:00
parent 48658e7618
commit 08d1995503
56 changed files with 8239 additions and 0 deletions

20
.babelrc Normal file
View File

@ -0,0 +1,20 @@
{
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"ie >= 11"
]
},
"modules": false,
"loose": true
}
]
],
"plugins": [
"@babel/plugin-proposal-optional-chaining"
]
}

5
.eslintignore Normal file
View File

@ -0,0 +1,5 @@
/node_modules
/package
/types
/coverage
/rollup.config.js

27
.eslintrc Normal file
View File

@ -0,0 +1,27 @@
/*
* @Author: early-autumn
* @Date: 2020-03-06 20:35:23
* @LastEditors: early-autumn
* @LastEditTime: 2020-04-17 12:39:07
*/
{
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"plugins": [
"@typescript-eslint",
"prettier"
],
"env": {
"node": true,
"es6": true
},
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-namespace": "off"
}
}

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/node_modules
/package
/package.zip
/types
/coverage
/yarn-error.log

10
.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"parser": "typescript"
}

11
.travis.yml Normal file
View File

@ -0,0 +1,11 @@
language: node_js
node_js: stable
cache:
directories:
- node_modules
install:
- npm install
script:
- npm run prettier
- npm run lint
- npm run coverage

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 初秋
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

514
README.md Normal file
View File

@ -0,0 +1,514 @@
# axios-miniprogram
[![build status](https://travis-ci.com/zjx0905/axios-miniprogram.svg?branch=master)](https://travis-ci.org/zjx0905/axios-miniprogram)
[![Coverage Status](https://coveralls.io/repos/github/zjx0905/axios-miniprogram/badge.svg?branch=master)](https://coveralls.io/github/zjx0905/axios-miniprogram?branch=master)
[![npm version](https://badge.fury.io/js/axios-miniprogram.svg)](https://badge.fury.io/js/axios-miniprogram)
[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
## 安装
```bash
$ yarn add axios-miniprogram
```
或者
```bash
$ npm i axios-miniprogram
```
## 简介
小程序平台专用请求库,实现了 [axios](https://github.com/axios/axios) 大部分功能,用法只存在少许差异,如果您是 [axios](https://github.com/axios/axios) 的老用户,那么不需要学习就可以直接上手使用。
* 支持 微信小程序、支付宝小程序、百度小程序、字节跳动小程序、QQ 小程序、uniapp。
* 支持 `Typescript`,健全的类型系统,智能的 `IDE` 提示。
* 支持 `Promise`
* 支持 拦截器。
* 支持 取消请求。
* 支持 自定义合法状态码。
* 支持 自定义参数序列化。
* 支持 自定义转换数据。
* 支持 自定义错误处理。
* 支持 自定义平台适配器
## 使用
### `axios(config)`
可以通过将相关配置传递给`axios`来发送请求。
```typescript
// 发送 GET 请求
axios({
method: 'get',
url: '/test',
params: { test: 1 }
}).then((response) => {
// 请求成功后做些什么
}).catch((error) => {
// 请求失败后做些什么
});
// 发送 POST 请求
axios({
method: 'post',
url: '/test',
data: { test: 1 }
}).then((response) => {
// 请求成功后做些什么
}).catch((error) => {
// 请求失败后做些什么
});
```
### `axios(url, config?)`
也可以通过直接把`url`传给`axios`来发送请求。
```typescript
// 默认发送 GET 请求
axios('/test/xxx').then((response) => {
// 请求成功后做些什么
}).catch((error) => {
// 请求失败后做些什么
});
// 发送 POST 请求
axios('/test/xxx', { method: 'post' }).then((response) => {
// 请求成功后做些什么
}).catch((error) => {
// 请求失败后做些什么
});
```
还可以使用请求方法的别名来简化请求。
* ##### axios.request(config)
* ##### axios.options(url, config?)
* ##### axios.get(url, params?, config?)
* ##### axios.head(url, params?, config?)
* ##### axios.post(url, data?, config?)
* ##### axios.put(url, data?, config?)
* ##### axios.delete(url, params?, config?)
* ##### axios.trace(url, config?)
* ##### axios.connect(url, config?)
常用例子,其他同理:
```typescript
// 发送 GET 请求
axios.get('/test');
// 携带参数
axios.get('/test', { test: 1 });
// 携带额外配置
axios.get('/test', { test: 1 }, {
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
});
// 发送 POST 请求
axios.post('/test');
// 携带数据
axios.post('/test', { test: 1 });
// 携带额外配置
axios.post('/test', { test: 1 }, {
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
});
```
## 配置`config`
非全平台兼容的属性只会在平台支持的情况下生效。
|参数|类型|默认值|说明|全平台兼容|
|:-|:-|:-|:-|:-|
|adapter|Function|[查看](https://github.com/early-autumn/axios-miniprogram/blob/master/src/defaults.ts)|自定义适配器|是|
|baseURL|String| |基础地址|是|
|url|String| |请求地址|是|
|method|String|get|请求方法| |
|params|Object| |请求参数|是|
|data|String/Object/ArrayBuffer| |请求数据|是|
|headers|Object|[查看](https://github.com/early-autumn/axios-miniprogram/blob/master/src/defaults.ts)|请求头|是|
|validateStatus|Function|[查看](https://github.com/early-autumn/axios-miniprogram/blob/master/src/defaults.ts)|自定义合法状态码|是|
|paramsSerializer|Function| |自定义参数序列化|是|
|transformRequest|Function/Array<.Function>| |自定义转换请求数据|是|
|transformResponse|Function/Array<.Function>| |自定义转换响应数据|是|
|errorHandler|Function| |自定义错误处理|是|
|cancelToken|Object| |取消令牌|是|
|timeout|Number|10000|超时时间| |
|dataType|String|json|响应数据格式|是|
|responseType|String|text|响应数据类型|是|
|enableHttp2|Boolean|false|开启 http2| |
|enableQuic|Boolean|false|开启 quic| |
|enableCache|Boolean|false|开启 cache| |
|sslVerify|Boolean|true|验证 ssl 证书| |
#### `config.method`的合法值
可以使用大写,也可以使用小写。
|值|说明|全平台兼容|
|:-|:-|:-|
|OPTIONS| | |
|GET| |是|
|HEAD| | |
|POST| |是|
|PUT| |是|
|DELETE| |是|
|TRACE| | |
|CONNECT| | |
#### `config.dataType`的合法值
|值|说明|全平台兼容|
|:-|:-|:-|
|json|返回的数据为 JSON返回后会对返回的数据进行一次 JSON.parse|是|
|其他|不对返回的内容进行 JSON.parse|是|
#### `config.responseType`的合法值
|值|说明|全平台兼容|
|:-|:-|:-|
|text|响应的数据为文本|是|
|arraybuffer|响应的数据为 ArrayBuffer|是|
#### 自定义合法状态码`config.validateStatus`
可以让请求按照您的要求成功或者失败。
```typescript
axios('/test', {
validateStatus: function validateStatus(status) {
// 这样,状态码在 200 到 400 之间都是请求成功
return status >= 200 && status < 400;
}
});
```
#### 自定义参数序列化`config.paramsSerializer`
可以使用自己的规则去序列化参数。
```typescript
axios('/test', {
paramsSerializer: function paramsSerializer(params) {
return qs.stringify(params, {arrayFormat: 'brackets'});
}
});
```
#### 自定义转换数据
可以在请求发出之前转换请求数据,在请求成功之后转换响应数据。
```typescript
axios('/test', {
transformRequest: [function transformRequest(data, headers) {
// 转换请求数据
return data;
}],
transformResponse: [function transformResponse(data) {
// 转换响应数据
return data;
}],
});
```
#### 自定义错误处理`config.errorHandler`
可以添加到默认配置中,统一处理错误。
```typescript
axios.defaults.errorHandler = function errorHandler(error) {
// 做一些想做的事情
return Promise.reject(error);
}
const instance = axios.create({
errorHandler: function errorHandler(error) {
// 做一些想做的事情
return Promise.reject(error);
}
});
```
也可以发送请求时通过自定义配置传入。
```typescript
axios('/test', {
errorHandler: function errorHandler(error) {
// 做一些想做的事情
return Promise.reject(error);
}
});
```
#### 自定义平台适配器`config.adapter`
您可以手动适配当前所处的平台。
```typescript
axios.defaults.adapter = function adapter(adapterConfig) {
const {
// 请求地址
url,
// 请求方法
method,
// 请求数据
data,
// 请求头 同 headers
header,
// 请求头 同 header
headers,
// 响应数据格式
dataType,
// 响应数据类型
responseType,
// 超时时间
timeout,
// 开启 http2
enableHttp2,
// 开启 quic
enableQuic,
// 开启 cache
enableCache,
// 验证 ssl 证书
sslVerify,
// 成功的回调函数
success,
// 失败的回调函数
fail
} = adapterConfig;
// 在 adapterConfig 中选择您需要的参数发送请求
return wx.request({
url,
method,
data,
header,
success,
fail
});
}
// 如果 adapterConfig 的数据结构适用于当前平台,则可以。
axios.defaults.adapter = wx.request;
```
#### 默认配置`defaults`
##### 全局默认配置`axios.defaults`
```typescript
axios.defaults.baseURL = 'https://www.xxx.com';
axios.defaults.headers.common['Accept'] = 'application/json, test/plain, */*';
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
```
##### 自定义实例默认配置
可以创建时传入。
```typescript
const instance = axios.create({
baseURL: 'https://www.xxx.com',
headers: {
common: {
'Accept': 'application/json, test/plain, */*'
},
post: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
}
}
});
```
也可以创建后修改。
```typescript
instance.defaults.baseURL = 'https://www.xxx.com';
instance.defaults.headers.common['Accept'] = 'application/json, test/plain, */*';
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
```
##### 配置优先顺序
发送请求时,会使用默认配置`defaults`和自定义配置`config`合并出请求配置`requestConfig`,然后用合并出的请求配置`requestConfig`去发送请求,多数情况下,后者优先级要高于前者,具体合并策略可以参考 [mergeConfig.ts](https://github.com/early-autumn/axios-miniprogram/blob/master/src/core/mergeConfig.ts) 的实现。
## 响应体`response`
非全平台兼容的属性只会在平台支持的情况下生效。
|属性|类型|说明|全平台兼容|
|:-|:-|:-|:-|
|status|Number|状态码|是|
|statusText|String|状态文本|是|
|data|String/Object/ArrayBuffer|开发者服务器返回的数据|是|
|headers|Object|响应头|是|
|config|Object|Axios 请求配置|是|
|cookies|Array<.String>|开发者服务器返回的 cookies格式为字符串数组| |
|profile|Object|网络请求过程中一些关键时间点的耗时信息| |
## API
### `axios.interceptors`
可以先拦截请求或响应然后再由then或catch处理。
```typescript
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
console.log('request');
return config;
}, function (error) {
//处理请求错误
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 请求成功后做些什么
console.log('response');
return response;
}, function (error) {
// 处理响应错误
return Promise.reject(error);
});
axios('/test').then(function (response){
console.log('ok');
});
// log 'request' 'response' 'ok'
```
如果以后需要删除拦截器,则可以。
```typescript
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
```
还可以将拦截器添加到`axios`的`自定义实例`中。
```typescript
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
```
### `axios.CancelToken`
可以使用`CancelToken`取消已经发出的请求。
```typescript
let cancel;
axios('/test', {
cancelToken: new axios.CancelToken(function (c){
cancel = c;
})
});
cancel('取消请求');
```
还可以使用`CancelToken.source`工厂方法创建`CancelToken`。
```typescript
const source = axios.CancelToken.source();
axios('/test', {
cancelToken: source.token
});
source.cancel('取消请求');
```
### `axios.isCancel`
可以判断当前错误是否来自取消请求
```typescript
axios('/test').catch((error) => {
if(axios.isCancel(error)){
// 请求被取消了
}
});
```
### `axios.getUri(config)`
根据配置中的`url`和`params`生成一个`URI`。
```typescript
// uri === '/test?id=1'
const uri = axios.getUri({
url: '/test',
params: { id: 1 }
});
```
### `axios.create(defaults)`
创建一个`自定义实例`,传入的自定义默认配置`defaults`会和`axios`的默认配置`axios.defaults`合并成`自定义实例`的默认配置。
`自定义实例`拥有和`axios`相同的调用方式和请求方法的别名。
```typescript
axios.defaults.baseURL = 'https://www.xxx.com';
const instance = axios.create({
params: {
id: 1
}
});
// 最终请求的 URL 是这样的 => https://www.xxx.com/test?id=1
// https://www.xxx.com 来自 axios.defaults.baseURL
// /test 来自传入的 '/test'
// id=1 来自 instance.defaults.params
instance('/test');
instance.get('/test');
```
### `axios.Axios`
`axios.Axios`是一个类,其实`axios`就是`axios.Axios`类的实例改造而来的,`axios.create(defaults)`创建的也是`axios.Axios`的实例。
直接实例化`axios.Axios`可以得到一个`原始实例`,不能当函数调用,传入的自定义配置就是`原始实例`的默认配置,而不会像`axios.create(defaults)`一样去合并`axios`中的默认配置。
```typescript
axios.defaults.baseURL = 'https://www.xxx.com';
const instance = new axios.Axios({
params: { value: 123 }
});
// 最终请求的 URL 是这样的 => /test?value=123
// /test 来自传入的 '/test'
// value=123 来自 instance.defaults.params
instance.get('/test');
```
## 执行流程
```typescript
axios('/test').then().catch();
// 请求成功
// axios => axios.interceptors.request => config.transformRequest => config.paramsSerializer => config.adapter => config.validateStatus => config.transformResponse => axios.interceptors.response => then
// 请求失败
// axios => axios.interceptors.request => config.transformRequest => config.paramsSerializer => config.adapter => config.validateStatus => config.transformResponse => config.errorHandler => axios.interceptors.response => catch
```

21
SECURITY.md Normal file
View File

@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

16
__tests__/.babelrc Normal file
View File

@ -0,0 +1,16 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-transform-modules-commonjs"
]
}

View File

@ -0,0 +1,28 @@
import adaptive from '../src/adaptive';
declare global {
namespace NodeJS {
interface Global {
wx: any;
}
}
}
describe('测试 src/adaptive.ts', () => {
it('适配成功', () => {
const request = jest.fn();
global.wx = {
request,
};
const adapter = adaptive();
expect(adapter).toBe(request);
});
it('适配失败', () => {
global.wx = void 0;
expect(adaptive()).toBeUndefined();
});
});

90
__tests__/axios.spec.ts Normal file
View File

@ -0,0 +1,90 @@
import axios from '../src/axios';
describe('测试 src/axios.ts', () => {
it('default', () => {
axios('/test').then(void 0, (error) => {
expect(error.isAxiosError).toBe(true);
expect(error.message).toBe('平台适配失败,您需要参阅文档使用自定义适配器手动适配当前平台');
});
});
it('axios call', async () => {
axios.defaults.adapter = (config): any => {
config.success({ status: 200, data: {}, headers: {} });
expect(config.method).toBe(config.url.toUpperCase().replace('/', ''));
return 'task';
};
await axios({ url: '/get' });
await axios('/get');
await axios.request({ url: '/get' });
await axios.options('options');
await axios.get('get');
await axios.head('head');
await axios.post('post');
await axios.put('put');
await axios.delete('delete');
await axios.trace('trace');
await axios.connect('connect');
});
it('axios 携带参数', async () => {
const url = '/test';
const params = {
id: 1,
};
axios.defaults.adapter = (config): any => {
config.success({ status: 200, data: {}, headers: {} });
expect(config.method).toBe('GET');
expect(config.url).toBe('/test?id=1');
return 'task';
};
await axios({
url,
params,
});
await axios(url, {
params,
});
await axios.get(url, params);
});
it('axios 携带数据', async () => {
const url = '/test';
const data = {
id: 1,
};
axios.defaults.adapter = (config): any => {
config.success({ status: 200, data: '', headers: {} });
expect(config.method).toBe('POST');
expect(config.url).toBe(url);
expect(config.data).toEqual(data);
return 'task';
};
await axios({
method: 'post',
url,
data,
});
await axios(url, {
method: 'post',
data,
});
await axios.post(url, data);
});
it('axios.create 工厂方法', () => {
const instance = axios.create();
expect(instance.defaults).toEqual(axios.defaults);
});
});

View File

@ -0,0 +1,15 @@
import Cancel from '../../src/cancel/Cancel';
describe('测试 src/cancel/Cancel.ts', () => {
it('默认', () => {
const cancel = new Cancel();
expect(cancel.toString()).toBe('Cancel');
});
it('自定义', () => {
const cancel = new Cancel('custom');
expect(cancel.toString()).toBe('Cancel: custom');
});
});

View File

@ -0,0 +1,27 @@
import CancelToken from '../../src/cancel/CancelToken';
describe('测试 src/cancel/CancelToken.ts', () => {
it('实例化', () => {
const token = new CancelToken(function (cancel) {
cancel('取消');
});
// 应该抛出取消
expect(() => token.throwIfRequested()).toThrow();
});
it('工厂方法', () => {
const source = CancelToken.source();
// 还没有取消 返回 Undefuned
expect(source.token.throwIfRequested()).toBeUndefined();
source.cancel('取消');
// 应该抛出取消
expect(() => source.token.throwIfRequested()).toThrow();
// 重复取消无效
source.cancel('取消');
});
});

View File

@ -0,0 +1,12 @@
import isCancel from '../../src/cancel/isCancel';
import Cancel from '../../src/cancel/Cancel';
describe('测试 src/cancel/isCancel', () => {
it('是一个取消?', () => {
const cancel1 = {};
const cancel2 = new Cancel();
expect(isCancel(cancel1)).toBe(false);
expect(isCancel(cancel2)).toBe(true);
});
});

View File

@ -0,0 +1,63 @@
import Axios from '../../src/core/Axios';
const instance = new Axios();
describe('测试 src/core/Axios.ts', () => {
it('defaults', () => {
expect(instance.defaults).toEqual({});
});
it('getUri', () => {
expect(instance.getUri({})).toEqual('');
expect(instance.getUri({ url: '/test' })).toEqual('/test');
expect(instance.getUri({ url: '', params: { id: 1 } })).toEqual('id=1');
expect(instance.getUri({ url: '/test', params: { id: 1 } })).toEqual('/test?id=1');
});
it('interceptors 成功', () => {
instance.defaults.adapter = function adapter({ data, success }): any {
expect(data).toBe('interceptors_request');
success({ data: 'data', headers: {} });
return 'task';
};
instance.interceptors.request.use(function (config) {
config.data = 'interceptors_request';
return config;
});
instance.interceptors.response.use(function (response) {
response.data = 'interceptors_response';
return response;
});
instance
.request({
method: 'post',
data: '',
})
.then(({ data }) => expect(data).toBe('interceptors_response'));
});
it('interceptors 失败', () => {
instance.interceptors.response.use((response) => Promise.reject(response));
instance
.request({
method: 'post',
data: '',
})
.then(void 0, (error) => expect(error.data).toBe('interceptors_response'));
instance.interceptors.request.use((config) => Promise.reject(config));
instance
.request({
method: 'post',
data: '',
})
.then(void 0, (error) => expect(error.method).toBe('post'));
});
});

View File

@ -0,0 +1,61 @@
import InterceptorManager from '../../src/core/InterceptorManager';
describe('测试 src/core/InterceptorManager.ts', () => {
it('实例化', () => {
const interceptor = new InterceptorManager();
const executor = jest.fn();
interceptor.forEach(executor);
// executor 不应该被执行
expect(executor.mock.calls.length).toBe(0);
});
it('注册和取消注册', () => {
const interceptor = new InterceptorManager();
const executor1 = jest.fn();
const executor2 = jest.fn();
const id1 = interceptor.use(() => void 0);
const id2 = interceptor.use(() => void 0);
interceptor.forEach(executor1);
// executor1 应该被执行了两次
expect(executor1.mock.calls.length).toBe(2);
interceptor.eject(id1);
interceptor.eject(id2);
interceptor.forEach(executor2);
// executor2 不应该被执行
expect(executor2.mock.calls.length).toBe(0);
});
it('倒序遍历', () => {
const interceptor = new InterceptorManager();
let id = 0;
// 应该后被执行
interceptor.use((id) => expect(id).toBe(1));
// 应该先被执行
interceptor.use((id) => expect(id).toBe(0));
interceptor.forEach(({ resolved }) => {
resolved(id++);
}, 'reverse');
});
it('异常', () => {
const interceptor = new InterceptorManager();
interceptor.use(
() => void 0,
(error: any) => {
expect(error).toBe('error');
return Promise.reject(error);
}
);
interceptor.forEach(({ rejected }) => rejected?.('error'));
});
});

View File

@ -0,0 +1,55 @@
import { CancelAction } from '../../src/types';
import dispatchRequest from '../../src/core/dispatchRequest';
import CancelToken from '../../src/cancel/CancelToken';
import isCancel from '../../src/cancel/isCancel';
describe('测试 src/core/dispatchRequest.ts', () => {
it('默认', () => {
dispatchRequest({}).then(void 0, (err) => {
expect(err.message).toBe('平台适配失败,您需要参阅文档使用自定义适配器手动适配当前平台');
});
});
it('请求失败', () => {
dispatchRequest({
adapter({ success }): any {
success({ status: 200, data: '' });
return 'task';
},
validateStatus(status) {
return status === -1;
},
}).then(void 0, (err) => expect(err.response.status).toBe(200));
});
it('自定义错误处理', () => {
dispatchRequest({
adapter({ fail }): any {
fail({});
return 'task';
},
errorHandler(error) {
error.errorHandler = true;
return error;
},
}).then(void 0, (error) => expect(error.errorHandler).toBe(true));
});
it('取消请求', () => {
let cancel: CancelAction;
dispatchRequest({
adapter({ success }) {
cancel();
setTimeout(() => {
success({ status: 200, data: '' });
});
},
cancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
}).then(void 0, (err) => expect(isCancel(err)).toBe(true));
});
});

View File

@ -0,0 +1,35 @@
import flattenHeaders from '../../src/core/flattenHeaders';
describe('测试 src/core/flattenHeaders.ts', () => {
it('默认', () => {
expect(flattenHeaders({})).toEqual({});
expect(flattenHeaders({ headers: {} })).toEqual({});
});
it('默认 get', () => {
expect(
flattenHeaders({
headers: {
common: { common: 'common' },
get: { get: 'get' },
post: { post: 'post' },
rest: 'rest',
},
})
).toEqual({ common: 'common', get: 'get', rest: 'rest' });
});
it('拉平', () => {
expect(
flattenHeaders({
method: 'post',
headers: {
common: { common: 'common' },
get: { get: 'get' },
post: { post: 'post' },
rest: 'rest',
},
})
).toEqual({ common: 'common', post: 'post', rest: 'rest' });
});
});

View File

@ -0,0 +1,57 @@
import { AxiosRequestConfig } from '../../src/types';
import mergeConfig from '../../src/core/mergeConfig';
import defaults from '../../src/defaults';
describe('测试 src/core/mergeConfig.ts', () => {
it('默认', () => {
expect(mergeConfig()).toEqual({});
expect(mergeConfig({ baseURL: 'https://www.xxx.com' })).toEqual({ baseURL: 'https://www.xxx.com' });
expect(mergeConfig(void 0, { baseURL: 'https://www.xxx.com' })).toEqual({ baseURL: 'https://www.xxx.com' });
});
it('只取 config2', () => {
const config2 = { url: 'https://www.config2.com', data: { config2: 0 } };
const config = mergeConfig(defaults, config2);
expect(config.url).toEqual(config2.url);
expect(config.data).toEqual(config2.data);
});
it('优先取 config2', () => {
expect(mergeConfig(defaults, {})).toEqual(defaults);
const config2: AxiosRequestConfig = {
baseURL: 'https://www.config2.com',
method: 'post',
timeout: 10000,
};
const config = mergeConfig(defaults, config2);
expect(config.baseURL).toEqual(config2.baseURL);
expect(config.method).toEqual(config2.method);
expect(config.timeout).toEqual(config2.timeout);
});
it('深度合并', () => {
const config1 = { params: { config1: 0 }, headers: { Config1: '0' } };
const config2 = { params: { config2: 0 }, headers: { Config2: '0' } };
expect(mergeConfig(config1, {})).toEqual(config1);
expect(mergeConfig(config1, config2)).toEqual({
params: { config1: 0, config2: 0 },
headers: { Config1: '0', Config2: '0' },
});
expect(mergeConfig({ params: {} }, { params: { config: 'config2' } })).toEqual({
params: { config: 'config2' },
});
expect(mergeConfig({ params: { config: 'config1' } }, {})).toEqual({
params: { config: 'config1' },
});
});
});

View File

@ -0,0 +1,36 @@
import request from '../../src/core/request';
import CancelToken from '../../src/cancel/CancelToken';
import isCancel from '../../src/cancel/isCancel';
describe('测试 src/core/request.ts', () => {
it('默认', () => {
request({}).then(void 0, (err) =>
expect(err.message).toBe('平台适配失败,您需要参阅文档使用自定义适配器手动适配当前平台')
);
});
it('请求失败', () => {
request({
adapter({ fail }): any {
fail({});
return 'task';
},
}).then(void 0, (err) => expect(err.message).toBe('配置不正确或者网络异常'));
});
it('取消请求', () => {
request({
adapter({ fail }) {
setTimeout(fail);
return {
abort: jest.fn(),
};
},
cancelToken: new CancelToken(function executor(c) {
c();
}),
}).then(void 0, (err) => expect(isCancel(err)).toBe(true));
});
});

View File

@ -0,0 +1,27 @@
import { Data } from '../../src/types';
import transformData from '../../src/core/transformData';
describe('测试 src/core/transformData.ts', () => {
it('默认', () => {
expect(transformData({ a: 1 }, {})).toEqual({ a: 1 });
});
it('转换', () => {
function transform(data: Data) {
return data + '1';
}
expect(transformData('1', {}, transform)).toEqual('11');
});
it('多次转换', () => {
const transforms = [
function transform(data: Data) {
return data + '1';
},
function transform(data: Data) {
return data + '1';
},
];
expect(transformData('1', {}, transforms)).toEqual('111');
});
});

View File

@ -0,0 +1,13 @@
import { methodToLowercase, methodToUppercase } from '../../src/core/transformMethod';
describe('测试 src/core/transformMethod.ts', () => {
it('默认', () => {
expect(methodToLowercase()).toBe('get');
expect(methodToUppercase()).toBe('GET');
});
it('传参', () => {
expect(methodToLowercase('POST')).toBe('post');
expect(methodToUppercase('post')).toBe('POST');
});
});

View File

@ -0,0 +1,42 @@
import transformRequest from '../../src/core/transformRequest';
describe('测试 src/core/transformRequest.ts', () => {
it('默认', () => {
expect(transformRequest({})).toEqual({
url: '/',
method: 'GET',
headers: void 0,
data: void 0,
dataType: void 0,
enableCache: void 0,
enableHttp2: void 0,
enableQuic: void 0,
header: void 0,
responseType: void 0,
sslVerify: void 0,
timeout: void 0,
});
});
it('基本', () => {
const request = transformRequest({
baseURL: 'https://www.xxx.com///',
method: 'get',
url: '/test',
params: {
id: 1,
},
});
const request2 = transformRequest({
baseURL: 'https://www.xxx.com',
method: 'get',
url: 'https://www.yyy.com/test',
params: {
id: 1,
},
});
expect(request.url).toEqual('https://www.xxx.com/test?id=1');
expect(request2.url).toEqual('https://www.yyy.com/test?id=1');
});
});

View File

@ -0,0 +1,38 @@
import transformResponse from '../../src/core/transformResponse';
describe('测试 src/core/transformResponse.ts', () => {
it('默认', () => {
expect(transformResponse({ data: {} }, {})).toEqual({
status: 400,
statusText: 'Bad Adapter',
data: {},
headers: {},
config: {},
cookies: void 0,
profile: void 0,
});
});
it('status + headers', () => {
expect(transformResponse({ status: 200, headers: { status: 'ok' }, data: {} }, {})).toEqual({
status: 200,
statusText: 'OK',
data: {},
headers: { status: 'ok' },
config: {},
cookies: void 0,
profile: void 0,
});
});
it('statusCode + header', () => {
expect(transformResponse({ statusCode: 204, header: { status: 'ok' }, data: {} }, {})).toEqual({
status: 204,
statusText: '',
data: {},
headers: { status: 'ok' },
config: {},
cookies: void 0,
profile: void 0,
});
});
});

View File

@ -0,0 +1,82 @@
import buildURL from '../../src/helpers/buildURL';
describe('测试 src/helpers/buildURL.ts', () => {
it('url', () => {
expect(buildURL('/test')).toBe('/test');
expect(buildURL('/test?id=1')).toBe('/test?id=1');
});
it('url + params', () => {
expect(
buildURL('/test', {
test: 1,
})
).toBe('/test?test=1');
expect(
buildURL('/test?id=1', {
test: 1,
})
).toBe('/test?id=1&test=1');
});
it('url + params + paramsSerializer', () => {
expect(
buildURL(
'/test',
{
test: 1,
},
() => 'paramsSerializer=ok'
)
).toBe('/test?paramsSerializer=ok');
expect(
buildURL(
'/test?id=1',
{
test: 1,
},
() => 'paramsSerializer=ok'
)
).toBe('/test?id=1&paramsSerializer=ok');
});
it('params 是数组', () => {
expect(
buildURL('/test', {
ids: [1],
})
).toBe('/test?ids[]=1');
});
it('params 是时间对象', () => {
const date = new Date();
expect(
buildURL('/test', {
date,
})
).toBe(`/test?date=${date.toISOString()}`);
});
it('params 是普通对象', () => {
const obj = {};
expect(
buildURL('/test', {
obj,
})
).toBe(`/test?obj=%7B%7D`);
});
it('删除哈希', () => {
expect(buildURL('/test#192929')).toBe('/test');
});
it('容错', () => {
expect(
buildURL('/test', {
null: null,
undefined: void 0,
NaN: NaN,
})
).toBe('/test');
});
});

View File

@ -0,0 +1,8 @@
import combineURL from '../../src/helpers/combineURL';
describe('测试 src/helpers/combineURL.ts', () => {
it('run', () => {
expect(combineURL('1/2', '3/4')).toBe('1/2/3/4');
expect(combineURL('1/2///', '////3/4')).toBe('1/2/3/4');
});
});

View File

@ -0,0 +1,10 @@
import isAbsoluteURL from '../../src/helpers/isAbsoluteURL';
describe('测试 src/helpers/isAbsoluteURL.ts', () => {
it('run', () => {
expect(isAbsoluteURL('1/2')).toBe(false);
expect(isAbsoluteURL('/1/2')).toBe(false);
expect(isAbsoluteURL('///1/2')).toBe(true);
expect(isAbsoluteURL('http://1/2')).toBe(true);
});
});

View File

@ -0,0 +1,41 @@
import { encode, isDate, isPlainObject, deepMerge, pick, omit } from '../../src/helpers/utils';
describe('测试 src/helpers/utils.ts', () => {
it('encode 特殊字符转换', () => {
expect(encode('@:, []$')).toBe('@:,+[]$');
});
it('isDate', () => {
expect(isDate(new Date())).toBe(true);
expect(isDate('')).toBe(false);
expect(isDate(Date)).toBe(false);
});
it('isPlainObject', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject(Object.create(null))).toBe(true);
expect(isPlainObject('')).toBe(false);
expect(isPlainObject(Object)).toBe(false);
});
it('deepMerge', () => {
expect(deepMerge({}, {})).toEqual({});
expect(deepMerge({ a: 0, b: '1', c: { a: 0, b: '1' } }, { a: 1, b: '1', c: { a: 1, b: '1' } })).toEqual({
a: 1,
b: '1',
c: { a: 1, b: '1' },
});
});
it('pick', () => {
expect(pick({})).toEqual({});
expect(pick({ a: 0, b: 0 }, 'a')).toEqual({ a: 0 });
});
it('omit', () => {
expect(omit({})).toEqual({});
expect(omit({ a: 0, b: 0 }, 'a')).toEqual({ b: 0 });
});
});

74
package.json Normal file
View File

@ -0,0 +1,74 @@
{
"name": "axios-miniprogram",
"version": "1.1.3",
"description": "基于 Promise 的 HTTP 请求库,适用于各大小程序平台。",
"main": "package/index.js",
"miniprogram": "package",
"types": "types/index.d.ts",
"files": [
"package",
"types"
],
"scripts": {
"build": "rollup -c",
"lint": "eslint --ext ts --fix src __tests__",
"prettier": "prettier --write --config .prettierrc \"{src,__tests__}/**/*.{js,ts}\"",
"test": "jest",
"test:watch": "yarn test -- --watch",
"test:cov": "yarn test -- --coverage",
"coverage": "yarn test:cov --coverageReporters=text-lcov | coveralls"
},
"husky": {
"hooks": {
"pre-commit": "yarn prettier && yarn lint && yarn test:cov && git add ."
}
},
"jest": {
"preset": "ts-jest",
"globals": {
"ts-jest": {
"babelConfig": "test/.babelrc"
}
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/zjx0905/axios-miniprogram.git"
},
"keywords": [
"axios",
"miniprogram",
"request",
"promise"
],
"author": "zjx0905",
"license": "MIT",
"bugs": {
"url": "https://github.com/zjx0905/axios-miniprogram/issues"
},
"homepage": "https://github.com/zjx0905/axios-miniprogram#readme",
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@babel/runtime": "^7.10.5",
"@types/jest": "^26.0.5",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"coveralls": "^3.1.0",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.2.5",
"jest": "^26.1.0",
"prettier": "^2.0.5",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-typescript2": "^0.27.1",
"ts-jest": "^26.1.3",
"typescript": "^3.9.7"
}
}

41
rollup.config.js Normal file
View File

@ -0,0 +1,41 @@
/*
* @Author: early-autumn
* @Date: 2020-03-06 20:40:30
* @LastEditors: early-autumn
* @LastEditTime: 2020-04-28 13:18:34
*/
import fs from 'fs';
import path from 'path';
import nodeResolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import typescript2 from 'rollup-plugin-typescript2';
function removeDir(name) {
try {
if (fs.statSync(name).isFile()) {
fs.unlinkSync(name);
} else {
fs.readdirSync(name).forEach((dir) => removeDir(path.join(name, dir)));
fs.rmdirSync(name)
}
} catch (err) {}
}
export default function() {
removeDir('package');
removeDir('types');
return {
input: 'src/index.ts',
output: {
file: 'package/index.js',
format: 'cjs',
indent: false,
},
plugins: [
nodeResolve({ extensions: ['.ts'] }),
typescript2({ useTsconfigDeclarationDir: true }),
babel({ extensions: ['.ts'] }),
],
};
}

38
src/adaptive.ts Normal file
View File

@ -0,0 +1,38 @@
import { Adapter, Platform } from './types';
// uniapp
declare let uni: Platform;
// 微信小程序
declare let wx: Platform;
// 支付宝小程序
declare let my: Platform;
// 百度小程序
declare let swan: Platform;
// 字节跳动小程序
declare let tt: Platform;
// QQ 小程序
declare let qq: Platform;
/**
*
*/
export default function adaptive(): Adapter | undefined {
const stack: (() => Adapter)[] = [
() => uni.request,
() => wx.request,
() => my.request,
() => swan.request,
() => tt.request,
() => qq.request,
];
let adapter: Adapter | undefined;
while (stack.length !== 0 && adapter === void 0) {
try {
adapter = (stack.shift() as () => Adapter)();
} catch (err) {}
}
return adapter;
}

62
src/axios.ts Normal file
View File

@ -0,0 +1,62 @@
import { AxiosRequestConfig, Data, AxiosResponse, AxiosBaseInstance, AxiosInstance } from './types';
import Axios from './core/Axios';
import mergeConfig from './core/mergeConfig';
import CancelToken from './cancel/CancelToken';
import isCancel from './cancel/isCancel';
import defaults from './defaults';
/**
* Axios
*
* Axios
*/
function createInstance(defaults: AxiosRequestConfig): AxiosInstance {
const instance = new Axios(defaults);
/**
* axios
*/
function axios<T extends Data>(
url: AxiosRequestConfig | string,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> {
// 调用方式一处理请求配置
if (typeof url !== 'string') {
config = url;
}
// 调用方式二处理请求配置
else {
config = { ...config, url };
}
return instance.request(config);
}
// instance 的属性合并到 axios 函数中
Object.assign(axios, instance);
// instance 的方法合并到 axios 函数中
Object.setPrototypeOf(axios, Object.getPrototypeOf(instance));
return axios as AxiosInstance;
}
/**
* Axios
*/
const axios = createInstance(defaults);
// 添加 create 工厂方法
axios.create = function create(defaults: AxiosRequestConfig = {}): AxiosBaseInstance {
return createInstance(mergeConfig(axios.defaults, defaults));
};
// 添加 Axios 类
axios.Axios = Axios;
// 添加 CancelToken 类
axios.CancelToken = CancelToken;
// 添加 检查错误是否来自取消请求 方法
axios.isCancel = isCancel;
export default axios;

14
src/cancel/Cancel.ts Normal file
View File

@ -0,0 +1,14 @@
import { Cancel } from '../types';
export default class CancelClass implements Cancel {
/**
* @param message
*/
public constructor(public message?: string) {}
public toString(): string {
const message = this.message ? `: ${this.message}` : '';
return `Cancel${message}`;
}
}

60
src/cancel/CancelToken.ts Normal file
View File

@ -0,0 +1,60 @@
import { CancelToken, CancelAction, CancelExecutor, CancelTokenSource } from '../types';
import Cancel from './Cancel';
export default class CancelTokenClass implements CancelToken {
/**
*
*/
private _reason?: Cancel;
public listener: Promise<Cancel>;
public constructor(executor: CancelExecutor) {
let action!: CancelAction;
this.listener = new Promise<Cancel>((resolve) => {
action = (message) => {
// 防止重复取消
if (this._reason) {
return;
}
this._reason = new Cancel(message);
resolve(this._reason);
};
});
executor(action);
}
public throwIfRequested(): void {
if (this._reason) {
throw this._reason;
}
}
/**
* CancelTokenSource
*
* CancelTokenSource.token CancelToken
*
* CancelTokenSource.cancel CancelAction
*
* CancelTokenSource.cancel('这里可以填写您的错误信息')
*
* CancelTokenSource.token
*/
public static source(): CancelTokenSource {
let cancel!: CancelAction;
const token = new CancelTokenClass(function executor(action) {
cancel = action;
});
return {
token,
cancel,
};
}
}

10
src/cancel/isCancel.ts Normal file
View File

@ -0,0 +1,10 @@
import Cancel from './Cancel';
/**
*
*
* @param value
*/
export default function isCancel(value: unknown): value is Cancel {
return value instanceof Cancel;
}

120
src/core/Axios.ts Normal file
View File

@ -0,0 +1,120 @@
import { Method, Params, Data, Interceptors, AxiosRequestConfig, AxiosResponse, Axios } from '../types';
import buildURL from '../helpers/buildURL';
import mergeConfig from './mergeConfig';
import InterceptorManager from './InterceptorManager';
import dispatchRequest from './dispatchRequest';
export default class AxiosClass implements Axios {
public interceptors: Interceptors = {
request: new InterceptorManager<AxiosRequestConfig>(),
response: new InterceptorManager<AxiosResponse>(),
};
/**
* @param defaults
*/
public constructor(public defaults: AxiosRequestConfig = {}) {}
public getUri(config: AxiosRequestConfig): string {
const { url = '', params, paramsSerializer } = mergeConfig(this.defaults, config);
return buildURL(url, params, paramsSerializer).replace(/^\?/, '');
}
public request<T extends Data>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
const requestConfig = mergeConfig(this.defaults, config);
let promiseRequest = Promise.resolve(requestConfig);
// 执行请求拦截器
this.interceptors.request.forEach(function executor({ resolved, rejected }) {
promiseRequest = promiseRequest.then(resolved, rejected);
}, 'reverse');
// 发送请求
let promiseResponse = promiseRequest.then(dispatchRequest);
// 执行响应拦截器
this.interceptors.response.forEach(function executor({ resolved, rejected }) {
promiseResponse = promiseResponse.then(resolved, rejected);
});
return promiseResponse as Promise<AxiosResponse<T>>;
}
public options<T extends Data>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutParams<T>('options', url, void 0, config);
}
public get<T extends Data>(url: string, params?: Params, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutParams<T>('get', url, params, config);
}
public head<T extends Data>(url: string, params?: Params, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutParams<T>('head', url, params, config);
}
public post<T extends Data>(url: string, data?: Data, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutData<T>('post', url, data, config);
}
public put<T extends Data>(url: string, data?: Data, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutData<T>('put', url, data, config);
}
public delete<T extends Data>(url: string, params?: Params, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutParams<T>('delete', url, params, config);
}
public trace<T extends Data>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutParams<T>('trace', url, void 0, config);
}
public connect<T extends Data>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this._requestMethodWithoutParams<T>('connect', url, void 0, config);
}
/**
* HTTP
*
* @param method
* @param url
* @param params
* @param config
*/
private _requestMethodWithoutParams<T extends Data>(
method: Method,
url: string,
params?: Params,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> {
return this.request<T>({
...config,
method,
url,
params,
});
}
/**
* HTTP
*
* @param method
* @param url
* @param data
* @param config
*/
private _requestMethodWithoutData<T extends Data>(
method: Method,
url: string,
data?: Data,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> {
return this.request<T>({
...config,
method,
url,
data,
});
}
}

View File

@ -0,0 +1,45 @@
import {
InterceptorResolved,
InterceptorRejected,
Interceptor,
InterceptorExecutor,
InterceptorManager,
} from '../types';
/**
*
*/
export default class InterceptorManagerClass<T> implements InterceptorManager<T> {
/**
* id
*/
private _id = 0;
/**
*
*/
private _interceptors: Record<number, Interceptor<T>> = {};
public use(resolved: InterceptorResolved<T>, rejected?: InterceptorRejected): number {
this._interceptors[++this._id] = {
resolved,
rejected,
};
return this._id;
}
public eject(id: number): void {
delete this._interceptors[id];
}
public forEach(executor: InterceptorExecutor<T>, reverse?: 'reverse'): void {
let interceptors: Interceptor<T>[] = Object.values(this._interceptors);
if (reverse === 'reverse') {
interceptors = interceptors.reverse();
}
interceptors.forEach(executor);
}
}

45
src/core/createError.ts Normal file
View File

@ -0,0 +1,45 @@
import { AxiosRequestConfig, RequestConfig, AxiosResponse, AxiosError } from '../types';
/**
* AxiosError Error
*/
class AxiosErrorClass extends Error implements AxiosError {
public isAxiosError = true;
/**
* @param message
* @param config Axios
* @param request
* @param response Axios
*/
public constructor(
message: string,
public config: AxiosRequestConfig,
public request: RequestConfig,
public response?: AxiosResponse
) {
super(message);
// 修复继承系统自带类 prototype 设置失败的问题
Object.setPrototypeOf(this, AxiosErrorClass.prototype);
}
}
/**
* AxiosError
*
* AxiosError
*
* @param message
* @param config Axios
* @param request
* @param response Axios
*/
export default function createError(
message: string,
config: AxiosRequestConfig,
request: RequestConfig,
response?: AxiosResponse
): AxiosError {
return new AxiosErrorClass(message, config, request, response);
}

View File

@ -0,0 +1,51 @@
import { AxiosRequestConfig, AxiosResponse } from '../types';
import isCancel from '../cancel/isCancel';
import flattenHeaders from './flattenHeaders';
import transformData from './transformData';
import request from './request';
/**
* ,
*
* @param config Axios
*/
function throwIfCancellationRequested(config: AxiosRequestConfig) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
/**
*
*
* @param config Axios
*/
export default function dispatchRequest(config: AxiosRequestConfig): Promise<AxiosResponse> {
throwIfCancellationRequested(config);
config.headers = flattenHeaders(config);
config.data = transformData(config.data ?? {}, config.headers, config.transformRequest);
function onResolved(response: AxiosResponse): AxiosResponse {
throwIfCancellationRequested(config);
response.data = transformData(response.data, response.headers, config.transformResponse);
return response;
}
function onRejected(reason: any): Promise<any> {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
if (reason.response !== void 0) {
reason.response.data = transformData(reason.response.data, reason.response.headers, config.transformResponse);
}
}
return config.errorHandler?.(reason) ?? Promise.reject(reason);
}
return request(config).then(onResolved, onRejected);
}

View File

@ -0,0 +1,24 @@
import { Headers, AxiosRequestConfig } from '../types';
import { omit } from '../helpers/utils';
import { methodToLowercase } from './transformMethod';
/**
*
*
* @param config Axios
*/
export default function flattenHeaders(config: AxiosRequestConfig): Headers {
const { headers } = config;
if (headers === void 0) {
return {};
}
const method = methodToLowercase(config.method);
return {
...(headers.common ?? {}),
...(headers[method] ?? {}),
...omit(headers, 'common', 'options', 'get', 'head', 'post', 'put', 'delete', 'trace', 'connect'),
};
}

122
src/core/mergeConfig.ts Normal file
View File

@ -0,0 +1,122 @@
import { AnyObject, AxiosRequestConfig } from '../types';
import { isPlainObject, deepMerge } from '../helpers/utils';
type OnlyFromConfig2Key = 'url' | 'data';
type PriorityFromConfig2Key =
| 'adapter'
| 'baseURL'
| 'method'
| 'validateStatus'
| 'paramsSerializer'
| 'transformRequest'
| 'transformResponse'
| 'errorHandler'
| 'cancelToken'
| 'dataType'
| 'responseType'
| 'timeout'
| 'enableHttp2'
| 'enableQuic'
| 'enableCache'
| 'sslVerify';
type DeepMergeConfigKey = 'params' | 'headers';
/**
* config2
*
* @param keys
* @param config
* @param config2
*/
function onlyFromConfig2(keys: OnlyFromConfig2Key[], config: AxiosRequestConfig, config2: AxiosRequestConfig) {
keys.forEach((key) => {
if (config2[key] !== void 0) {
config[key] = config2[key] as any;
}
});
}
/**
* config2 , config2 config1
*
* @param keys
* @param config
* @param config1
* @param config2
*/
function priorityFromConfig2(
keys: PriorityFromConfig2Key[],
config: AxiosRequestConfig,
config1: AxiosRequestConfig,
config2: AxiosRequestConfig
) {
keys.forEach((key) => {
if (config2[key] !== void 0) {
config[key] = config2[key] as any;
} else if (config1[key] !== void 0) {
config[key] = config1[key] as any;
}
});
}
/**
*
*
* @param keys
* @param config
* @param config1
* @param config2
*/
function deepMergeConfig(
keys: DeepMergeConfigKey[],
config: AxiosRequestConfig,
config1: AxiosRequestConfig,
config2: AxiosRequestConfig
) {
keys.forEach((key) => {
if (isPlainObject(config2[key])) {
config[key] = deepMerge(config1[key] ?? {}, config2[key] as AnyObject);
} else if (isPlainObject(config1[key])) {
config[key] = deepMerge(config1[key] as AnyObject);
}
});
}
/**
* Axios
*
* @param config1 Axios 1
* @param config2 Axios 2
*/
export default function mergeConfig(
config1: AxiosRequestConfig = {},
config2: AxiosRequestConfig = {}
): AxiosRequestConfig {
const config: AxiosRequestConfig = {};
const onlyFromConfig2Keys: OnlyFromConfig2Key[] = ['url', 'data'];
const priorityFromConfig2Keys: PriorityFromConfig2Key[] = [
'adapter',
'baseURL',
'method',
'validateStatus',
'paramsSerializer',
'transformRequest',
'transformResponse',
'errorHandler',
'cancelToken',
'dataType',
'responseType',
'timeout',
'enableHttp2',
'enableQuic',
'enableCache',
'sslVerify',
];
const deepMergeConfigKeys: DeepMergeConfigKey[] = ['headers', 'params'];
onlyFromConfig2(onlyFromConfig2Keys, config, config2);
priorityFromConfig2(priorityFromConfig2Keys, config, config1, config2);
deepMergeConfig(deepMergeConfigKeys, config, config1, config2);
return config;
}

70
src/core/request.ts Normal file
View File

@ -0,0 +1,70 @@
import { AxiosRequestConfig, AxiosResponse, Response } from '../types';
import transformRequest from './transformRequest';
import transformResponse from './transformResponse';
import createError from './createError';
/**
*
*
* @param config Axios
*/
export default function request(config: AxiosRequestConfig): Promise<AxiosResponse> {
return new Promise(function dispatchAdapter(resolve, reject): void {
const { adapter, cancelToken } = config;
const requestConfig = transformRequest(config);
/**
*
*
* @param message
* @param response Axios
*/
function catchError(message: any, response?: AxiosResponse): void {
if (typeof message !== 'string') {
message = '配置不正确或者网络异常';
}
reject(createError(message, config, requestConfig, response));
}
if (adapter === void 0) {
catchError('平台适配失败,您需要参阅文档使用自定义适配器手动适配当前平台');
return;
}
/**
*
*
* @param res
*/
function handleResponse(res: Response): void {
const response = transformResponse(res, config);
if (config.validateStatus === void 0 || config.validateStatus(response.status)) {
resolve(response);
} else {
catchError(`请求失败,状态码为 ${response.status}`, response);
}
}
// 使用适配器发送请求
const task = adapter({
...requestConfig,
success: handleResponse,
fail: catchError,
});
// 如果存在取消令牌
// 则调用取消令牌里的 listener 监听用户的取消操作
if (cancelToken !== void 0) {
cancelToken.listener.then(function onCanceled(reason): void {
if (task !== void 0) {
task.abort();
}
reject(reason);
});
}
});
}

28
src/core/transformData.ts Normal file
View File

@ -0,0 +1,28 @@
import { Data, Headers, TransformData } from '../types';
/**
*
*
* @param data /
* @param headers /
* @param transforms /
*/
export default function transformData(
data: Data,
headers: Headers,
transforms?: TransformData | TransformData[]
): Data {
if (transforms === void 0) {
return data;
}
if (!Array.isArray(transforms)) {
transforms = [transforms];
}
transforms.forEach((transform: TransformData) => {
data = transform(data, headers);
});
return data;
}

View File

@ -0,0 +1,19 @@
import { AliasMethod, AdapterMethod, Method } from '../types';
/**
*
*
* @param config Axios
*/
export function methodToLowercase(method: Method = 'get'): AliasMethod {
return method.toLowerCase() as AliasMethod;
}
/**
*
*
* @param config Axios
*/
export function methodToUppercase(method: Method = 'GET'): AdapterMethod {
return method.toUpperCase() as AdapterMethod;
}

View File

@ -0,0 +1,48 @@
import { AxiosRequestConfig, RequestConfig } from '../types';
import { pick } from '../helpers/utils';
import isAbsoluteURL from '../helpers/isAbsoluteURL';
import combineURL from '../helpers/combineURL';
import buildURL from '../helpers/buildURL';
import { methodToUppercase } from './transformMethod';
/**
* baseURL url params URL
*
* @param config Axios
*/
function transformURL(config: AxiosRequestConfig): string {
const { baseURL = '', url = '' } = config;
const fullURL = isAbsoluteURL(url) ? url : combineURL(baseURL, url);
return buildURL(fullURL, config.params, config.paramsSerializer);
}
/**
* Axios
*
*
*
* @param config Axios
*/
export default function transformRequest(config: AxiosRequestConfig): RequestConfig {
return {
url: transformURL(config),
method: methodToUppercase(config.method),
header: config.headers,
...pick(
config,
'data',
'headers',
'dataType',
'responseType',
'timeout',
'enableHttp2',
'enableQuic',
'enableCache',
'sslVerify'
),
} as RequestConfig;
}

View File

@ -0,0 +1,26 @@
import { AxiosRequestConfig, AxiosResponse, Response } from '../types';
import { pick } from '../helpers/utils';
/**
* Axios
*
*
*
* @param response
* @param config Axios
*/
export default function transformResponse(response: Response, config: AxiosRequestConfig): AxiosResponse {
const status = response.statusCode ?? response.status ?? 400;
const headers = response.header ?? response.headers ?? {};
const statusText = status === 200 ? 'OK' : status === 400 ? 'Bad Adapter' : '';
return {
status,
statusText,
headers,
config,
...pick(response, 'data', 'cookies', 'profile'),
};
}

81
src/defaults.ts Normal file
View File

@ -0,0 +1,81 @@
import { AxiosRequestConfig } from './types';
import adaptive from './adaptive';
const defaults: AxiosRequestConfig = {
/**
*
*/
adapter: adaptive(),
/**
*
*/
method: 'get',
/**
*
*/
headers: {
common: {
Accept: 'application/json, test/plain, */*',
},
options: {},
get: {},
head: {},
post: {
'Context-Type': 'application/x-www-form-urlencoded; charset=utf-8',
},
put: {
'Context-Type': 'application/x-www-form-urlencoded; charset=utf-8',
},
delete: {},
trace: {},
connect: {},
},
/**
*
*
* @param status
*/
validateStatus: function validateStatus(status: number): boolean {
return status >= 200 && status < 300;
},
/**
*
*/
timeout: 10000,
/**
*
*/
dataType: 'json',
/**
*
*/
responseType: 'text',
/**
* http2
*/
enableHttp2: false,
/**
* quic
*/
enableQuic: false,
/**
* cache
*/
enableCache: false,
/**
* ssl
*/
sslVerify: true,
};
export default defaults;

74
src/helpers/buildURL.ts Normal file
View File

@ -0,0 +1,74 @@
import { AnyObject, Params } from '../types';
import { encode, isPlainObject, isDate } from './utils';
/**
*
*
* @param url
* @param serializedParams
*/
function generateURL(url: string, serializedParams: string): string {
// 移除 hash
const hashIndex = url.indexOf('#');
if (hashIndex !== -1) {
url = url.slice(0, hashIndex);
}
if (serializedParams === '') {
return url;
}
// 拼接前缀
const prefix = url.indexOf('?') === -1 ? '?' : '&';
serializedParams = `${prefix}${serializedParams}`;
return `${url}${serializedParams}`;
}
/**
*
*
* @param params
*/
function paramsSerialization(params: AnyObject): string {
const parts: string[] = [];
Object.entries(params).forEach(function encodeKeyValue([key, value]): void {
if (value === null || value === void 0 || value !== value) {
return;
}
// 如果值是一个数组, 则特殊处理 key
if (Array.isArray(value)) {
key += '[]';
}
// 转成数组统一处理
const values: any[] = [].concat(value);
values.forEach((val: any): void => {
if (isPlainObject(val)) {
val = JSON.stringify(val);
} else if (isDate(val)) {
val = val.toISOString();
}
parts.push(`${encode(key)}=${encode(val)}`);
});
});
return parts.join('&');
}
/**
* URL
*
* @param url
* @param params
* @param paramsSerializer
*/
export default function buildURL(url: string, params: Params = {}, paramsSerializer = paramsSerialization): string {
return generateURL(url, paramsSerializer(params));
}

View File

@ -0,0 +1,8 @@
/**
* baseURL url URL
*
* combineURL('1/2///','////3/4') => '1/2/3/4'
*/
export default function combineURL(baseURL: string, url: string): string {
return `${baseURL.replace(/\/*$/, '')}/${url.replace(/^\/*/, '')}`;
}

View File

@ -0,0 +1,10 @@
/**
* URL
*
* xxx:// 或者 "//" 开头, 视为绝对地址
*
* @param url URL
*/
export default function isAbsoluteURL(url: string): boolean {
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
}

104
src/helpers/utils.ts Normal file
View File

@ -0,0 +1,104 @@
import { AnyObject } from '../types';
const _toString = Object.prototype.toString;
/**
*
*
* @param str
*/
export function encode(str: string): string {
return encodeURIComponent(str)
.replace(/%40/gi, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, '+')
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']');
}
/**
*
*
* @param date
*/
export function isDate(date: unknown): date is Date {
return _toString.call(date) === '[object Date]';
}
/**
*
*
* @param obj
*/
export function isPlainObject(obj: unknown): obj is Record<string, unknown> {
return _toString.call(obj) === '[object Object]';
}
/**
*
*
* @param objs n
*/
export function deepMerge(...objs: Record<string, any>[]): Record<string, any> {
const result: Record<string, any> = {};
function assignValue(key: string, val: any) {
// 如果当前结果和当前值都为普通对象
// 递归进行深度合并
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = deepMerge(result[key], val);
}
// 如果只有当前值为普通对象
// 直接深拷贝当前值
else if (isPlainObject(val)) {
result[key] = deepMerge({}, val);
}
// 如果都不是普通对象
// 直接赋值
else {
result[key] = val;
}
}
objs.forEach(function assignObj(obj: Record<string, any>): void {
Object.entries(obj).forEach(function assignKey([key, value]) {
assignValue(key, value);
});
});
return result;
}
/**
*
*
* @param obj
* @param keys key
*/
export function pick<T extends AnyObject, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const _pick: Partial<T> = {};
keys.forEach(function pickKey(key: K) {
_pick[key] = obj[key];
});
return _pick as Pick<T, K>;
}
/**
*
*
* @param obj
* @param keys key
*/
export function omit<T extends AnyObject, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> {
const _omit = { ...obj };
keys.forEach(function omitKey(key: K) {
delete _omit[key];
});
return _omit;
}

5
src/index.ts Normal file
View File

@ -0,0 +1,5 @@
import axios from './axios';
export * from './types';
export default axios;

699
src/types.ts Normal file
View File

@ -0,0 +1,699 @@
/**
*
*/
export declare interface AnyObject<T extends any = any> {
[x: string]: T;
}
/**
*
*/
export declare type AdapterMethod = 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT';
/**
*
*/
export declare type AliasMethod = 'options' | 'get' | 'head' | 'post' | 'put' | 'delete' | 'trace' | 'connect';
/**
* Axios
*/
export declare type Method = AliasMethod | AdapterMethod;
/**
* Axios
*/
export declare type Params = AnyObject;
/**
* Axios
*/
export declare type Data = string | AnyObject | ArrayBuffer;
/**
* Axios
*/
export declare interface Headers extends Partial<Record<'common' | AliasMethod, AnyObject<string>>> {
/**
*
*/
[x: string]: AnyObject<string> | string | undefined;
}
/**
*
*/
export declare interface RequestConfig {
/**
*
*/
url: string;
/**
* HTTP
*/
method: AdapterMethod;
/**
*
*/
data: Data;
/**
* headers
*/
header: AnyObject;
/**
* header
*/
headers: AnyObject;
/**
*
*/
dataType?: 'json' | '其他';
/**
*
*/
responseType?: 'text' | 'arraybuffer';
/**
*
*/
timeout?: number;
/**
* http2
*/
enableHttp2?: boolean;
/**
* quic
*/
enableQuic?: boolean;
/**
* cache
*/
enableCache?: boolean;
/**
* ssl
*/
sslVerify?: boolean;
}
/**
*
*/
export declare interface Response {
/**
*
*/
statusCode?: number;
/**
*
*/
status?: number;
/**
* Headers
*/
header?: AnyObject;
/**
* Headers
*/
headers?: Headers;
/**
*
*/
data: Data;
/**
* cookies
*/
cookies?: string[];
/**
*
*/
profile?: AnyObject;
}
/**
*
*/
export declare interface AdapterRequestConfig extends RequestConfig {
/**
*
*/
success: (res: Response) => void;
/**
*
*/
fail: (err: unknown) => void;
}
/**
*
*/
export declare interface AdapterRequestTask {
/**
*
*/
abort(): void;
}
/**
*
*/
export declare interface Adapter {
(config: AdapterRequestConfig): AdapterRequestTask | void;
}
/**
*
*/
export declare interface Platform {
request: Adapter;
}
/**
*
*/
export declare interface TransformData {
(data: Data, headers: Headers): Data;
}
/**
*
*/
export declare interface ErrorHandler {
(error: any): Promise<any> | any;
}
/**
* Axios
*/
export declare interface AxiosRequestConfig {
/**
*
*/
adapter?: Adapter;
/**
*
*/
baseURL?: string;
/**
*
*/
url?: string;
/**
*
*/
method?: Method;
/**
*
*/
params?: Params;
/**
*
*/
data?: Data;
/**
*
*/
headers?: Headers;
/**
*
*/
validateStatus?: (status: number) => boolean;
/**
*
*/
paramsSerializer?: (params?: AnyObject) => string;
/**
*
*/
transformRequest?: TransformData | TransformData[];
/**
*
*/
transformResponse?: TransformData | TransformData[];
/**
*
*/
errorHandler?: ErrorHandler;
/**
*
*/
cancelToken?: CancelToken;
/**
*
*/
timeout?: number;
/**
*
*/
dataType?: 'json' | '其他';
/**
*
*/
responseType?: 'text' | 'arraybuffer';
/**
* http2
*/
enableHttp2?: boolean;
/**
* quic
*/
enableQuic?: boolean;
/**
* cache
*/
enableCache?: boolean;
/**
* ssl
*/
sslVerify?: boolean;
}
/**
* Axios
*/
export declare interface AxiosResponse<T extends Data = Data> {
/**
*
*/
status: number;
/**
*
*/
statusText: string;
/**
*
*/
data: T;
/**
*
*/
headers: Headers;
/**
* Axios
*/
config: AxiosRequestConfig;
/**
* cookies
*/
cookies?: string[];
/**
*
*/
profile?: AnyObject;
}
/**
*
*/
export declare interface InterceptorResolved<T = any> {
(value: T): Promise<T> | T;
}
/**
*
*/
export declare interface InterceptorRejected {
(error: any): any;
}
/**
*
*/
export declare interface Interceptor<T = any> {
/**
*
*/
resolved: InterceptorResolved<T>;
/**
*
*/
rejected?: InterceptorRejected;
}
/**
*
*/
export declare interface InterceptorExecutor<T = any> {
(interceptor: Interceptor<T>): void;
}
/**
*
*/
export declare interface InterceptorManager<T = any> {
[x: string]: any;
/**
*
*
* @param resolved
* @param rejected
*/
use(resolved: InterceptorResolved<T>, rejected?: InterceptorRejected): number;
/**
*
*
* @param id id
*/
eject(id: number): void;
/**
*
*
* @param executor
* @param reverse
*/
forEach(executor: InterceptorExecutor<T>, reverse?: 'reverse'): void;
}
/**
* Axios
*/
export declare interface Interceptors {
/**
* request
*/
request: InterceptorManager<AxiosRequestConfig>;
/**
* response
*/
response: InterceptorManager<AxiosResponse>;
}
/**
* Axios
*/
export declare interface Axios {
/**
*
*/
defaults: AxiosRequestConfig;
/**
* Axios
*/
interceptors: Interceptors;
/**
* url params URI
*
* @param config
*/
getUri(config: AxiosRequestConfig): string;
/**
* HTTP
*
* @param config
*/
request<T extends Data>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP OPTIONS
*
* @param url
* @param config
*/
options<T extends Data>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP GET
*
* @param url
* @param params
* @param config
*/
get<T extends Data>(url: string, params?: Params, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP HEAD
*
* @param url
* @param params
* @param config
*/
head<T extends Data>(url: string, params?: Params, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP POST
*
* @param url
* @param data
* @param config
*/
post<T extends Data>(url: string, data?: Data, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP PUT
*
* @param url
* @param data
* @param config
*/
put<T extends Data>(url: string, data?: Data, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP DELETE
*
* @param url
* @param params
* @param config
*/
delete<T extends Data>(url: string, params?: Params, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP TRACE
*
* @param url
* @param config
*/
trace<T extends Data>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP CONNECT
*
* @param url
* @param config
*/
connect<T extends Data>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
}
/**
* Axios
*/
export declare interface AxiosConstructor {
new (config?: AxiosRequestConfig): Axios;
}
/**
* AxiosError Error
*/
export declare interface AxiosError extends Error {
/**
* Axios
*/
isAxiosError: boolean;
/**
*
*/
config: AxiosRequestConfig;
/**
*
*/
request: RequestConfig;
/**
* Axios
*/
response?: AxiosResponse;
}
/**
*
*/
export declare interface Cancel {
/**
*
*/
message?: string;
/**
*
*/
toString(): string;
}
/**
*
*/
export declare interface CancelConstructor {
new (message?: string): Cancel;
}
/**
*
*/
export declare interface CancelAction {
(message?: string): void;
}
/**
*
*/
export declare interface CancelExecutor {
(cancel: CancelAction): void;
}
/**
*
*/
export declare interface CancelToken {
/**
*
*/
listener: Promise<Cancel>;
/**
* ,
*/
throwIfRequested(): void;
}
/**
* source
*/
export declare interface CancelTokenSource {
/**
*
*/
token: CancelToken;
/**
*
*/
cancel: CancelAction;
}
/**
*
*/
export declare interface CancelTokenConstructor {
new (executor: CancelExecutor): CancelToken;
/**
* CancelTokenSource
*
* CancelTokenSource.token CancelToken
*
* CancelTokenSource.cancel CancelAction
*
* CancelTokenSource.cancel('这里可以填写您的错误信息')
*
* CancelTokenSource.token
*/
source(): CancelTokenSource;
}
/**
* Axios
*
* *
*/
export declare interface AxiosBaseInstance extends Axios {
/**
* HTTP
*
*
*
* @param config
*/
<T extends Data>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>;
/**
* HTTP
*
*
*
* @param url
* @param config
*/
<T extends Data>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
}
/**
* Axios
*
* *
*
* *
*/
export declare interface AxiosInstance extends AxiosBaseInstance {
/**
* Axios
*
* @param defaults
*/
create(defaults?: AxiosRequestConfig): AxiosBaseInstance;
/**
* Axios
*/
Axios: AxiosConstructor;
/**
*
*/
CancelToken: CancelTokenConstructor;
/**
*
*
* @param value
*/
isCancel: (value: any) => boolean;
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": [
"es2020"
],
"declaration": true,
"declarationDir": "./types",
"sourceMap": true,
"removeComments": false,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"moduleResolution": "node",
"baseUrl": "./",
"esModuleInterop": true,
"skipLibCheck": true,
},
"include": [
"./src/**/*"
]
}

4947
yarn.lock Normal file

File diff suppressed because it is too large Load Diff