chore: 初始化

pull/41/head
zjx0905 2023-03-23 20:09:00 +08:00
commit d7474504e7
43 changed files with 7728 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

48
.commitlintrc.cjs Normal file
View File

@ -0,0 +1,48 @@
const { execSync } = require('child_process');
const metas = [
{ type: 'feat', section: '✨ Features | 新功能' },
{ type: 'fix', section: '🐛 Bug Fixes | Bug 修复' },
{ type: 'test', section: '✅ Tests | 测试' },
{ type: 'docs', section: '📝 Documentation | 文档' },
{ type: 'build', section: '👷‍ Build System | 构建' },
{ type: 'ci', section: '🔧 Continuous Integration | CI 配置' },
{ type: 'perf', section: '⚡ Performance Improvements | 性能优化' },
{ type: 'revert', section: '⏪ Reverts | 回退' },
{ type: 'chore', section: '📦 Chores | 其他更新' },
{ type: 'style', section: '💄 Styles | 风格', hidden: true },
{ type: 'refactor', section: '♻ Code Refactoring | 代码重构' },
];
/** @type {import('cz-git').UserConfig} */
module.exports = {
rules: {
// @see: https://commitlint.js.org/#/reference-rules
'subject-min-length': [0, 'always', 3],
'subject-max-length': [0, 'always', 80],
'type-enum': [0, 'always', metas.map((meta) => meta.type)],
},
prompt: {
messages: {
type: '请选择提交类型:',
subject: '请输入变更描述:\n',
footer: '列举关联的 issue (可选) 例如: #31, #I3244\n',
confirmCommit: '是否提交?',
},
types: metas.map((meta) => ({
value: meta.type,
name: `${`${meta.type}:`.padEnd(10, ' ')}${meta.section}`,
})),
skipQuestions: ['scope', 'body', 'breaking', 'footerPrefix'],
formatMessageCB: (commit) =>
`${commit?.defaultMessage}\n\nCo-authored-by: ${readGitUser(
'name',
)} <${readGitUser('email')}>`,
},
};
function readGitUser(key) {
return execSync(`git config user.${key}`)
.toString()
.replace(/(\r\n\t|\n|\r\t)/g, '');
}

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

13
.eslintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"env": {
"es2022": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"root": true,
"rules": {
"@typescript-eslint/no-explicit-any": 0
}
}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
.eslintcache

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v16

10
.prettierrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always"
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 初秋
Permission is hereby granted, free of charge, to unknown 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.

571
README.md Normal file
View File

@ -0,0 +1,571 @@
# axios-miniprogram
[![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
```
## 简介
为小程序平台量身定制的轻量级请求库,请求配置以微信小程序作为标准,其他平台兼容实现。
- 支持 微信小程序、支付宝小程序、百度小程序、字节跳动小程序、QQ 小程序、uniapp。
- 支持 `Typescript`,健全的类型系统,智能的 `IDE` 提示。
- 支持 `Promise`
- 支持 拦截器。
- 支持 取消请求。
- 支持 自定义合法状态码。
- 支持 自定义参数序列化。
- 支持 自定义转换数据。
- 支持 自定义错误处理。
- 支持 自定义平台适配器
## 使用
### 如何引入
```typescript
// esm
import axios from 'axios-miniprogram';
// cjs
const axios = require('axios-miniprogram');
// 使用
axios('/user');
```
### `axios(config)`
可以通过将相关配置传递给`axios`来发送请求。
```typescript
// 发送 GET 请求
axios({
url: '/user',
method: 'get',
params: {
id: 1,
},
})
.then((response) => {
// 请求成功后做些什么
})
.catch((error) => {
// 请求失败后做些什么
});
// 发送 POST 请求
axios({
url: '/user',
method: 'post',
data: {
id: 1,
},
})
.then((response) => {
// 请求成功后做些什么
})
.catch((error) => {
// 请求失败后做些什么
});
```
### `axios(url, config?)`
也可以通过直接把`url`传给`axios`来发送请求。
```typescript
// 默认发送 GET 请求
axios('/user')
.then((response) => {
// 请求成功后做些什么
})
.catch((error) => {
// 请求失败后做些什么
});
// 发送 POST 请求
axios('/user', {
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('/user');
// 携带参数
axios.get('/user', {
test: 1,
});
// 携带额外配置
axios.get(
'/user',
{
id: 1,
},
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
},
);
// 发送 POST 请求
axios.post('/user');
// 携带数据
axios.post('/user', {
id: 1,
});
// 携带额外配置
axios.post(
'/user',
{
id: 1,
},
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
},
);
```
## 动态 URL
您可以使用动态 URL 提高 RESTful API 的书写体验。
```typescript
axios.get('/user/:id', {
id: 1,
});
// url => '/user/1?id=1'
```
## 配置`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('/user', {
validateStatus: function validateStatus(status) {
// 这样,状态码在 200 到 400 之间都是请求成功
return status >= 200 && status < 400;
},
});
```
#### 自定义参数序列化`config.paramsSerializer`
可以使用自己的规则去序列化参数。
```typescript
axios('/user', {
paramsSerializer: function paramsSerializer(params) {
return qs.stringify(params, {
arrayFormat: 'brackets',
});
},
});
```
#### 自定义转换数据
可以在请求发出之前转换请求数据,在请求成功之后转换响应数据。
```typescript
axios('/user', {
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('/user', {
errorHandler: function errorHandler(error) {
// 做一些想做的事情
return Promise.reject(error);
},
});
```
#### 自定义平台适配器`config.adapter`
您可以手动适配当前所处的平台。
```typescript
axios.defaults.adapter = function adapter(adapterConfig) {
const {
// 请求地址
url,
// 请求方法
method,
// 请求数据
data,
// 请求头 同 headers
headers,
// 请求头 同 headers
headers,
// 响应数据格式
dataType,
// 响应数据类型
responseType,
// 超时时间
timeout,
// 开启 http2
enableHttp2,
// 开启 quic
enableQuic,
// 开启 cache
enableCache,
// 验证 ssl 证书
sslVerify,
// 成功的回调函数
success,
// 失败的回调函数
fail,
} = adapterConfig;
// 在 adapterConfig 中选择您需要的参数发送请求
return wx.request({
url,
method,
data,
headers,
success,
fail,
});
};
// 如果 adapterConfig 的数据结构适用于当前平台,则可以。
axios.defaults.adapter = wx.request;
```
#### 默认配置`defaults`
##### 全局默认配置`axios.defaults`
```typescript
axios.defaults.baseURL = 'https://www.api.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.api.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.api.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) {
// 在发送请求之前做些什么
return config;
},
function (error) {
//处理请求错误
return Promise.reject(error);
},
);
// 添加响应拦截器
axios.interceptors.response.use(
function (response) {
// 请求成功后做些什么
return response;
},
function (error) {
// 处理响应错误
return Promise.reject(error);
},
);
```
如果以后需要删除拦截器,则可以。
```typescript
const myInterceptor = axios.interceptors.request.use(function () {
// 在发送请求之前做些什么
});
axios.interceptors.request.eject(myInterceptor);
```
还可以将拦截器添加到`axios`的`自定义实例`中。
```typescript
const myInterceptor = instance.interceptors.request.use(function () {
// 在发送请求之前做些什么
});
instance.interceptors.request.eject(myInterceptor);
```
### `axios.CancelToken`
可以使用`CancelToken`取消已经发出的请求。
```typescript
let cancel;
axios('/api', {
cancelToken: new axios.CancelToken(function (c) {
cancel = c;
}),
});
cancel('取消请求');
```
还可以使用`CancelToken.source`工厂方法创建`CancelToken`。
```typescript
const source = axios.CancelToken.source();
axios('/api', {
cancelToken: source.token,
});
source.cancel('取消请求');
```
### `axios.isCancel`
可以判断当前错误是否来自取消请求
```typescript
axios('/user').catch((error) => {
if (axios.isCancel(error)) {
// 请求被取消了
}
});
```
### `axios.getUri(config)`
根据配置中的`url`和`params`生成一个`URI`。
```typescript
const uri = axios.getUri({
url: '/user',
params: {
id: 1,
},
});
// '/user?id=1'
```
### `axios.create(defaults)`
创建一个`自定义实例`,传入的自定义默认配置`defaults`会和`axios`的默认配置`axios.defaults`合并成`自定义实例`的默认配置。
`自定义实例`拥有和`axios`相同的调用方式和请求方法的别名。
```typescript
axios.defaults.baseURL = 'https://www.api.com';
const instance = axios.create({
params: {
id: 1,
},
});
instance('/user');
// 'https://www.api.com/user?id=1'
```
### `axios.Axios`
`axios.Axios`是一个类,其实`axios`就是`axios.Axios`类的实例改造而来的,`axios.create(defaults)`创建的也是`axios.Axios`的实例。
直接实例化`axios.Axios`可以得到一个`原始实例`,不能当函数调用,传入的自定义配置就是`原始实例`的默认配置,而不会像`axios.create(defaults)`一样去合并`axios`中的默认配置。
```typescript
const instance = new axios.Axios({
beseURL: 'https://www.api.com',
params: {
id: 1,
},
});
instance.get('/user');
// 'https://www.api.com/user?id=1'
```
## 执行流程
```typescript
axios('/user').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
```

View File

@ -0,0 +1,6 @@
const pkg = require('../../package.json');
module.exports = {
title: pkg.name,
description: pkg.description,
};

1
docs/index.md Normal file
View File

@ -0,0 +1 @@
# Hello

11
global.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
declare const uni: unknown;
declare const wx: unknown;
declare const my: unknown;
declare const swan: unknown;
declare const tt: unknown;
declare const qq: unknown;
declare const qh: unknown;
declare const ks: unknown;
declare const dd: unknown;
declare type AnyObject<T = any> = Record<string, T>;

79
package.json Normal file
View File

@ -0,0 +1,79 @@
{
"name": "axios-miniprogram",
"type": "module",
"version": "2.0.0.alpha.0",
"description": "基于 Promise 的 HTTP 请求库,适用于各大小程序平台。",
"main": "dist/axios-miniprogram.esm.js",
"module": "dist/axios-miniprogram.cjs.js",
"types": "dist/axios-miniprogram.d.ts",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git+https://github.com/fluffff/axios-miniprogram.git"
},
"keywords": [
"axios",
"miniprogram",
"request",
"promise"
],
"author": "fluffff",
"bugs": {
"url": "https://github.com/fluffff/axios-miniprogram/issues"
},
"homepage": "https://github.com/fluffff/axios-miniprogram#readme",
"license": "MIT",
"engines": {
"node": ">=16"
},
"scripts": {
"cz": "czg",
"build": "esno scripts/build.ts",
"release": "esno scripts/release.ts",
"lint": "eslint --cache .",
"lint:fix": "pnpm lint --fix",
"postinstall": "simple-git-hooks"
},
"devDependencies": {
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@esbuild-kit/cjs-loader": "^2.4.2",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.5",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"archiver": "^5.3.1",
"consola": "^2.15.3",
"cz-git": "1.3.8",
"czg": "1.3.8",
"enquirer": "^2.3.6",
"eslint": "^8.36.0",
"esno": "^0.16.3",
"jest": "^29.5.0",
"lint-staged": "13.2.0",
"minimist": "^1.2.8",
"prettier": "2.8.5",
"rimraf": "^4.4.0",
"rollup": "^3.20.0",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",
"semver": "^7.3.8",
"simple-git-hooks": "^2.8.1",
"typescript": "^5.0.2",
"vitepress": "1.0.0-alpha.60"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged",
"commit-msg": "pnpm commitlint --edit $1"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"lint-staged": {
"*.(j|t)s": "pnpm lint:fix"
}
}

5049
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

55
rollup.config.js Normal file
View File

@ -0,0 +1,55 @@
import path from 'node:path';
import esbuild from 'rollup-plugin-esbuild';
import dtsPlugin from 'rollup-plugin-dts';
import { __dirname, getPkgJSON } from './scripts/utils.js';
const pkg = getPkgJSON();
const inputPath = path.resolve(__dirname, 'src/index.ts');
const outputPath = path.resolve(__dirname, 'dist');
const sourceMap = process.env.SOURCE_MAP === 'true';
const dts = process.env.DTS === 'true';
export default main();
function main() {
const configs = [buildConfig('esm'), buildConfig('cjs')];
if (dts) {
configs.push(buildConfig('dts'));
}
return configs;
}
function buildConfig(format) {
const isDts = format === 'dts';
const output = {
file: resolvePath(format, isDts),
format: isDts ? 'es' : format,
name: pkg.name,
exports: 'default',
indent: false,
sourcemap: isDts ? false : sourceMap,
};
return {
input: inputPath,
output,
plugins: [
isDts
? dtsPlugin({
tsconfig: path.resolve(__dirname, 'tsconfig.build.json'),
})
: esbuild({
tsconfig: path.resolve(__dirname, 'tsconfig.build.json'),
sourceMap: output.sourcemap,
minify: true,
}),
],
};
}
function resolvePath(format, isDts) {
return path.resolve(
outputPath,
`${pkg.name}${isDts ? '.d.ts' : `.${format}.js`}`,
);
}

22
scripts/build.ts Normal file
View File

@ -0,0 +1,22 @@
import minimist from 'minimist';
import consola from 'consola';
import { exec } from './utils';
const args = minimist(process.argv.slice(2));
const watching = Boolean(args.watch || args.w);
const release = Boolean(args.release || args.r);
const sourceMap = release || Boolean(args.sourceMap || args.s);
const dts = release || Boolean(args.dts || args.d);
build();
function build() {
exec('rimraf dist');
consola.info('构建产物');
exec(
`rollup -c ${
watching ? '-w' : ''
} --environment SOURCE_MAP:${sourceMap},DTS:${dts}`,
);
}

114
scripts/release.ts Normal file
View File

@ -0,0 +1,114 @@
import fs from 'node:fs';
import semver from 'semver';
import enquirer from 'enquirer';
import consola from 'consola';
import { exec, pkgPath, getPkgJSON } from './utils';
const pkg = getPkgJSON();
const { version: currentVersion } = pkg;
main().catch((err) => {
updateVersion(currentVersion);
console.error(err);
});
async function main() {
checkBranch();
const version = await inputVersion();
consola.info(`Update version ${currentVersion} -> ${version}`);
updateVersion(version);
consola.info('Git add');
exec('git add .');
consola.info('Git push');
exec(`git commit -m "chore: release v${version}"`);
exec('git push');
consola.info('Git push tag');
exec(`git tag -a v${version} -m "v${version}"`);
exec(`git push origin v${version}`);
}
function checkBranch() {
const releaseBranch = 'main';
const currentBranch = exec('git branch --show-current', {
stdio: 'pipe',
}).toString();
if (currentBranch !== releaseBranch) {
consola.warn(`请切回 ${releaseBranch} 分支进行发版!`);
process.exit();
}
}
async function inputVersion() {
const releases = createReleases();
let targetVersion: string;
const { release } = await enquirer.prompt<{ release: string }>({
type: 'select',
name: 'release',
message: '请选择版本类型',
choices: [...releases, 'custom'],
});
if (release === 'custom') {
const { version } = await enquirer.prompt<{
version: string;
}>({
type: 'input',
name: 'version',
message: '请输入自定义版本号',
initial: currentVersion,
});
targetVersion = version;
} else {
targetVersion = (release.match(/\((.+)\)/) as string[])[1];
}
if (
!semver.valid(targetVersion) ||
!semver.lt(currentVersion, targetVersion)
) {
consola.error(`无效的版本号: ${targetVersion}`);
process.exit();
}
const { yes: confirmRelease } = await enquirer.prompt<{ yes: boolean }>({
type: 'confirm',
name: 'yes',
message: `确定发布 v${targetVersion} `,
});
if (!confirmRelease) {
consola.error(`取消发布: v${targetVersion}`);
process.exit();
}
return targetVersion;
}
function createReleases() {
const types = [
['patch'],
['minor'],
['major'],
['prepatch', 'alpha'],
['preminor', 'alpha'],
['premajor', 'alpha'],
['prerelease', 'alpha'],
];
const releases: string[] = [];
for (const [type, preid] of types) {
releases.push(`${type} (${semver.inc(currentVersion, type, preid)})`);
}
return releases;
}
function updateVersion(version: string) {
pkg.version = version;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
}

11
scripts/utils.js Normal file
View File

@ -0,0 +1,11 @@
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
export const __dirname = fileURLToPath(new URL('../', import.meta.url));
export const require = createRequire(import.meta.url);
export const exec = (command, options) =>
execSync(command, { stdio: 'inherit', ...(options ?? {}) });
export const pkgPath = path.resolve(__dirname, 'package.json');
export const getPkgJSON = () => require(pkgPath);

37
scripts/zip.ts Normal file
View File

@ -0,0 +1,37 @@
// import fs from 'node:fs';
// import path from 'node:path';
// import archiver from 'archiver';
// import { getPkgJSON } from './utils';
// const pkg = getPkgJSON();
// const zip = archiver.create('zip', {});
// const distPath = path.resolve(__dirname, '..', 'dist');
// const distZipName = `download/${pkg.version}.zip`;
// const distZipPath = path.resolve(__dirname, '..', distZipName);
// function main() {
// const outputStream = fs.createWriteStream(distZipPath);
// const startTime = +new Date();
// zip.glob()
// zip.on('error', (error) => {
// throw error;
// });
// zip.on('finish', () => {
// const endTime = +new Date();
// const ss = ((endTime - startTime) / 1000).toFixed(1);
// });
// zip.pipe(outputStream);
// zip.directory(distPath, pkg.name);
// zip.finalize();
// }
// function getFilePaths(){
// }
// function createZipName(){
// return
// }

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

59
src/axios.ts Normal file
View File

@ -0,0 +1,59 @@
import { AxiosAdapter, createAdapter, AxiosPlatform } from './core/adapter';
import Axios, {
AxiosConstructor,
AxiosRequestConfig,
AxiosResponse,
} from './core/Axios';
import { CancelToken, CancelTokenConstructor, isCancel } from './core/cancel';
import { mergeConfig } from './core/mergeConfig';
import { isString } from './helpers/is';
import defaults from './defaults';
export interface AxiosInstance extends Axios {
<TData = unknown>(config: AxiosRequestConfig): Promise<AxiosResponse<TData>>;
<TData = unknown>(url: string, config?: AxiosRequestConfig): Promise<
AxiosResponse<TData>
>;
}
export interface AxiosStatic extends AxiosInstance {
Axios: AxiosConstructor;
CancelToken: CancelTokenConstructor;
create(defaults?: AxiosRequestConfig): AxiosInstance;
createAdapter(platform: AxiosPlatform): AxiosAdapter;
isCancel(value: unknown): boolean;
}
function createInstance(defaults: AxiosRequestConfig): AxiosInstance {
const instance = new Axios(defaults);
function axios<TData = unknown>(
url: AxiosRequestConfig | string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
if (isString(url)) {
config = Object.assign({}, config, { url });
} else {
config = url;
}
return instance.request(config);
}
Object.assign(axios, instance);
Object.setPrototypeOf(axios, Object.getPrototypeOf(instance));
return axios as AxiosInstance;
}
const axios = createInstance(defaults) as AxiosStatic;
axios.Axios = Axios;
axios.CancelToken = CancelToken;
axios.create = function create(defaults: AxiosRequestConfig): AxiosInstance {
return createInstance(mergeConfig(axios.defaults, defaults));
};
axios.createAdapter = createAdapter;
axios.isCancel = isCancel;
export default axios;

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

@ -0,0 +1,256 @@
import { buildURL } from '../helpers/url';
import { mergeConfig } from './mergeConfig';
import {
AxiosAdapterRequestMethod,
AxiosAdapter,
AxiosAdapterTask,
} from './adapter';
import { CancelToken } from './cancel';
import dispatchRequest from './dispatchRequest';
import InterceptorManager from './InterceptorManager';
import { AxiosTransformer } from './transformData';
export type AxiosRequestMethodAlias =
| 'options'
| 'get'
| 'head'
| 'post'
| 'put'
| 'delete'
| 'trace'
| 'connect';
export type AxiosRequestMethod =
| AxiosAdapterRequestMethod
| AxiosRequestMethodAlias;
export type AxiosRequestHeaders = AnyObject;
export type AxiosRequestParams = AnyObject;
export type AxiosRequestData = AnyObject;
export type AxiosResponseHeaders = AnyObject;
export interface AxiosRequestFormData extends AxiosRequestData {
fileName: string;
filePath: string;
fileType?: 'image' | 'video' | 'audio';
hideLoading?: boolean;
}
export interface AxiosProgressEvent {
progress: number;
totalBytesSent: number;
totalBytesExpectedToSend: number;
}
export interface AxiosProgressCallback {
(event: AxiosProgressEvent): void;
}
export interface AxiosRequestConfig {
adapter?: AxiosAdapter;
baseURL?: string;
cancelToken?: CancelToken;
data?: AxiosRequestData | AxiosRequestFormData | AxiosRequestFormData;
dataType?: 'json' | '其他';
download?: boolean;
enableHttp2?: boolean;
enableQuic?: boolean;
enableCache?: boolean;
errorHandler?: (error: unknown) => Promise<unknown>;
headers?: AxiosRequestHeaders;
method?: AxiosRequestMethod;
onUploadProgress?: AxiosProgressCallback;
onDownloadProgress?: AxiosProgressCallback;
params?: AxiosRequestParams;
paramsSerializer?: (params?: AxiosRequestParams) => string;
responseType?: 'text' | 'arraybuffer';
sslVerify?: boolean;
transformRequest?: AxiosTransformer | AxiosTransformer[];
transformResponse?: AxiosTransformer | AxiosTransformer[];
timeout?: number;
upload?: boolean;
url?: string;
validateStatus?: (status: number) => boolean;
}
export interface AxiosResponse<TData = unknown> {
status: number;
statusText: string;
headers: AxiosResponseHeaders;
data: TData;
config?: AxiosRequestConfig;
request?: AxiosAdapterTask;
cookies?: string[];
profile?: AnyObject;
}
export interface AxiosResponseError extends AnyObject {
status: number;
statusText: string;
headers: AxiosResponseHeaders;
config?: AxiosRequestConfig;
request?: AxiosAdapterTask;
}
export interface AxiosConstructor {
new (config: AxiosRequestConfig): Axios;
}
export default class Axios {
public defaults: AxiosRequestConfig;
public interceptors = {
request: new InterceptorManager<AxiosRequestConfig>(),
response: new InterceptorManager<AxiosResponse>(),
};
public constructor(defaults: AxiosRequestConfig = {}) {
this.defaults = defaults;
}
public getUri(config: AxiosRequestConfig): string {
const { url, params, paramsSerializer } = mergeConfig(
this.defaults,
config,
);
return buildURL(url, params, paramsSerializer).replace(/^\?/, '');
}
public request<TData = unknown>(
config: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
const requestConfig = mergeConfig(this.defaults, config);
let promiseRequest = Promise.resolve(requestConfig);
this.interceptors.request.forEach(({ resolved, rejected }) => {
promiseRequest = promiseRequest.then(
resolved,
rejected,
) as Promise<AxiosRequestConfig>;
}, 'reverse');
let promiseResponse = promiseRequest.then(dispatchRequest);
this.interceptors.response.forEach(({ resolved, rejected }) => {
promiseResponse = promiseResponse.then(resolved, rejected) as Promise<
AxiosResponse<unknown>
>;
});
return promiseResponse as Promise<AxiosResponse<TData>>;
}
public options<TData = unknown>(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutParams<TData>(
'options',
url,
undefined,
config,
);
}
public get<TData = unknown>(
url: string,
params?: AxiosRequestParams,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutParams<TData>('get', url, params, config);
}
public head<TData = unknown>(
url: string,
params?: AxiosRequestParams,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutParams<TData>('head', url, params, config);
}
public post<TData = unknown>(
url: string,
data?: AxiosRequestData | AxiosRequestFormData,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutData<TData>('post', url, data, config);
}
public put<TData = unknown>(
url: string,
data?: AxiosRequestData | AxiosRequestFormData,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutData<TData>('put', url, data, config);
}
public delete<TData = unknown>(
url: string,
params?: AxiosRequestParams,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutParams<TData>(
'delete',
url,
params,
config,
);
}
public trace<TData = unknown>(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutParams<TData>(
'trace',
url,
undefined,
config,
);
}
public connect<TData = unknown>(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this._requestMethodWithoutParams<TData>(
'connect',
url,
undefined,
config,
);
}
private _requestMethodWithoutParams<TData = unknown>(
method: AxiosRequestMethod,
url: string,
params?: AxiosRequestParams,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this.request<TData>({
...(config ?? {}),
method,
url,
params,
});
}
private _requestMethodWithoutData<TData = unknown>(
method: AxiosRequestMethod,
url: string,
data?: AxiosRequestData | AxiosRequestFormData,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return this.request<TData>({
...(config ?? {}),
method,
url,
data,
});
}
}

View File

@ -0,0 +1,50 @@
export interface InterceptorResolved<T = unknown> {
(value: T): T | Promise<T>;
}
export interface InterceptorRejected {
(error: unknown): unknown | Promise<unknown>;
}
export interface Interceptor<T = unknown> {
resolved: InterceptorResolved<T>;
rejected?: InterceptorRejected;
}
export interface InterceptorExecutor {
(interceptor: Interceptor): void;
}
export default class InterceptorManager<T = unknown> {
private id = 0;
private interceptors: AnyObject<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, reverse?: 'reverse'): void {
let interceptors: Interceptor<any>[] = Object.keys(this.interceptors).map(
(id) => this.interceptors[id],
);
if (reverse === 'reverse') {
interceptors = interceptors.reverse();
}
interceptors.forEach(executor);
}
}

292
src/core/adapter.ts Normal file
View File

@ -0,0 +1,292 @@
import {
isEmptyArray,
isFunction,
isPlainObject,
isString,
isUndefined,
} from '../helpers/is';
import { assert, throwError } from '../helpers/utils';
import {
AxiosProgressCallback,
AxiosRequestConfig,
AxiosRequestData,
AxiosRequestFormData,
AxiosRequestHeaders,
AxiosResponse,
AxiosResponseError,
} from './Axios';
export type AxiosAdapterRequestType = 'request' | 'download' | 'upload';
export type AxiosAdapterRequestMethod =
| 'OPTIONS'
| 'GET'
| 'HEAD'
| 'POST'
| 'PUT'
| 'DELETE'
| 'TRACE'
| 'CONNECT';
export interface AxiosAdapterRequestConfig extends AxiosRequestConfig {
type: AxiosAdapterRequestType;
method: AxiosAdapterRequestMethod;
url: string;
success(response: AxiosResponse): void;
fail(error: AxiosResponseError): void;
}
export interface AxiosAdapterBaseOptions extends AxiosAdapterRequestConfig {
header?: AxiosRequestHeaders;
success(response: unknown): void;
fail(error: unknown): void;
}
export interface AxiosAdapterUploadOptions extends AxiosAdapterBaseOptions {
filePath: string;
name: string;
fileName: string;
fileType: 'image' | 'video' | 'audio';
hideLoading?: boolean;
formData?: AxiosRequestData;
}
export interface AxiosAdapterDownloadOptions extends AxiosAdapterBaseOptions {
filePath?: string;
fileName?: string;
}
export interface AxiosAdapterRequest {
(config: AxiosAdapterBaseOptions): AxiosAdapterTask | void;
}
export interface AxiosAdapterUpload {
(config: AxiosAdapterUploadOptions): AxiosAdapterTask | void;
}
export interface AxiosAdapterDownload {
(config: AxiosAdapterDownloadOptions): AxiosAdapterTask | void;
}
export interface AxiosPlatform {
request: AxiosAdapterRequest;
upload: AxiosAdapterUpload;
download: AxiosAdapterDownload;
}
export interface AxiosAdapterTask {
abort?(): void;
onProgressUpdate?(callback: AxiosProgressCallback): void;
offProgressUpdate?(callback: AxiosProgressCallback): void;
}
export interface AxiosAdapter {
(config: AxiosAdapterRequestConfig): AxiosAdapterTask | void;
}
export function getAdapterDefault(): AxiosAdapter | undefined {
const tryGetPlatforms = [
() => uni,
() => wx,
() => my,
() => swan,
() => tt,
() => qq,
() => qh,
() => ks,
() => dd,
];
let platform;
while (!isEmptyArray(tryGetPlatforms) && !isPlatform(platform)) {
try {
const tryGetPlatform = tryGetPlatforms.shift();
if (isPlainObject((platform = tryGetPlatform?.()))) {
platform = revisePlatformApiNames(platform);
}
} catch (err) {
// 避免出现异常导致程序被终止
}
}
if (!isPlatform(platform)) {
return;
}
return createAdapter(platform);
}
export function createAdapter(platform: AxiosPlatform): AxiosAdapter {
assert(isPlainObject(platform), 'platform 需要是一个 object');
assert(isFunction(platform.request), 'platform.request 需要是一个 function');
assert(isFunction(platform.upload), 'platform.upload 需要是一个 function');
assert(
isFunction(platform.download),
'platform.download 需要是一个 function',
);
function adapterDefault(
config: AxiosAdapterRequestConfig,
): AxiosAdapterTask | void {
const baseOptions = transformOptions(config);
switch (config.type) {
case 'request':
return callRequest(platform.request, baseOptions);
case 'upload':
return callUpload(platform.upload, baseOptions);
case 'download':
return callDownload(platform.download, baseOptions);
default:
throwError(`无法识别的请求类型 ${config.type}`);
}
}
function callRequest(
request: AxiosAdapterRequest,
baseOptions: AxiosAdapterBaseOptions,
): AxiosAdapterTask | void {
return request(baseOptions);
}
function callUpload(
upload: AxiosAdapterUpload,
baseOptions: AxiosAdapterBaseOptions,
): AxiosAdapterTask | void {
assert(
isPlainObject(baseOptions.data),
'上传文件时 data 需要是一个 object',
);
assert(
isString(baseOptions.data?.fileName),
'上传文件时 data.fileName 需要是一个 string',
);
assert(
isString(baseOptions.data?.filePath),
'上传文件时 data.filePath 需要是一个 string',
);
const { fileName, filePath, fileType, hideLoading, ...formData } =
baseOptions.data as AxiosRequestFormData;
const options = {
...baseOptions,
name: fileName,
fileName: fileName,
filePath,
fileType: fileType ?? 'image',
hideLoading,
formData,
};
return upload(options);
}
function callDownload(
download: AxiosAdapterDownload,
baseOptions: AxiosAdapterBaseOptions,
): AxiosAdapterTask | void {
const options = {
...baseOptions,
filePath: baseOptions.params?.filePath,
fileName: baseOptions.params?.fileName,
success(response: AnyObject): void {
injectDownloadData(response);
baseOptions.success(response);
},
};
return download(options);
}
function transformResult(result: AnyObject): void {
if (!isUndefined(result.statusCode)) {
result.status = result.statusCode;
delete result.statusCode;
}
if (isUndefined(result.status)) {
result.status = isUndefined(result.data) ? 400 : 200;
}
if (!isUndefined(result.header)) {
result.headers = result.header;
delete result.header;
}
if (isUndefined(result.headers)) {
result.headers = {};
}
if (!isUndefined(result.errMsg)) {
result.statusText = result.errMsg;
delete result.errMsg;
}
if (isUndefined(result.statusText)) {
result.statusText =
result.status === 200
? 'OK'
: result.status === 400
? 'Bad Adapter'
: '';
}
}
function transformOptions(
config: AxiosAdapterRequestConfig,
): AxiosAdapterBaseOptions {
return {
...config,
header: config.headers,
success(response: AxiosResponse<unknown>): void {
transformResult(response);
config.success(response);
},
fail(error: AxiosResponseError): void {
transformResult(error);
config.fail(error);
},
};
}
function injectDownloadData(response: AnyObject): void {
if (!isPlainObject(response.data)) {
response.data = {};
}
if (!isUndefined(response.tempFilePath)) {
response.data.tempFilePath = response.tempFilePath;
delete response.tempFilePath;
}
if (!isUndefined(response.apFilePath)) {
response.data.tempFilePath = response.apFilePath;
delete response.apFilePath;
}
if (!isUndefined(response.filePath)) {
response.data.filePath = response.filePath;
delete response.filePath;
}
}
return adapterDefault;
}
export function isPlatform(value: unknown): value is AxiosPlatform {
return (
isPlainObject(value) &&
isFunction(value.request) &&
isFunction(value.upload) &&
isFunction(value.download)
);
}
export function revisePlatformApiNames(platform: AnyObject): AxiosPlatform {
return {
request: platform.request ?? platform.httpRequest,
upload: platform.upload ?? platform.uploadFile,
download: platform.download ?? platform.downloadFile,
};
}

80
src/core/cancel.ts Normal file
View File

@ -0,0 +1,80 @@
export interface CancelAction {
(message?: string): void;
}
export interface CancelExecutor {
(cancel: CancelAction): void;
}
export interface CancelTokenSource {
token: CancelToken;
cancel: CancelAction;
}
export interface CancelTokenConstructor {
new (executor: CancelExecutor): CancelToken;
source(): CancelTokenSource;
}
export class Cancel {
public message?: string;
public constructor(message?: string) {
this.message = message;
}
public toString(): string {
const message = this.message ? `: ${this.message}` : '';
return `Cancel${message}`;
}
}
export function isCancel(value: unknown): value is Cancel {
return value instanceof Cancel;
}
export class 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 static source(): CancelTokenSource {
let cancel!: CancelAction;
const token = new CancelToken((action) => {
cancel = action;
});
return {
token,
cancel,
};
}
public throwIfRequested(): void {
if (this.reason) {
throw this.reason;
}
}
}
export function isCancelToken(value: unknown): value is CancelToken {
return value instanceof CancelToken;
}

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

@ -0,0 +1,38 @@
import { AxiosAdapterTask } from './adapter';
import { AxiosRequestConfig, AxiosResponse, AxiosResponseError } from './Axios';
export type AxiosErrorResponse = AxiosResponse | AxiosResponseError;
class AxiosError extends Error {
public isAxiosError = true;
public config: AxiosRequestConfig;
public request?: AxiosAdapterTask;
public response?: AxiosErrorResponse;
public constructor(
message: string,
config: AxiosRequestConfig,
request?: AxiosAdapterTask,
response?: AxiosErrorResponse,
) {
super(message);
this.config = config;
this.request = request;
this.response = response;
Object.setPrototypeOf(this, AxiosError.prototype);
}
}
export function createError(
message: string,
config: AxiosRequestConfig,
request?: AxiosAdapterTask,
response?: AxiosErrorResponse,
): AxiosError {
return new AxiosError(message, config, request, response);
}

View File

@ -0,0 +1,56 @@
import { isPlainObject } from '../helpers/is';
import { isCancel } from './cancel';
import { flattenHeaders } from './flattenHeaders';
import { transformData } from './transformData';
import { request } from './request';
import { AxiosRequestConfig, AxiosResponse } from './Axios';
import { transformURL } from './transformURL';
function throwIfCancellationRequested(config: AxiosRequestConfig) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
export default function dispatchRequest<TData = unknown>(
config: AxiosRequestConfig,
): Promise<AxiosResponse> {
throwIfCancellationRequested(config);
config.url = transformURL(config);
config.headers = flattenHeaders(config);
config.data = transformData(
config.data,
config.headers,
config.transformRequest,
);
return request<TData>(config).then(
(response: AxiosResponse<TData>) => {
throwIfCancellationRequested(config);
response.data = transformData(
response.data as AnyObject,
response.headers,
config.transformResponse,
) as TData;
return response;
},
(reason: unknown) => {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
if (isPlainObject(reason) && isPlainObject(reason.response)) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse,
);
}
}
throw config.errorHandler?.(reason) ?? reason;
},
);
}

View File

@ -0,0 +1,34 @@
import { isPlainObject } from '../helpers/is';
import { omit, toLowerCase } from '../helpers/utils';
import {
AxiosRequestConfig,
AxiosRequestMethodAlias,
AxiosRequestHeaders,
} from './Axios';
export function flattenHeaders(
config: AxiosRequestConfig,
): AxiosRequestHeaders | undefined {
if (!isPlainObject(config.headers)) {
return config.headers;
}
return {
...(config.headers.common ?? {}),
...(config.headers[
toLowerCase<AxiosRequestMethodAlias>(config.method, 'get')
] ?? {}),
...omit(
config.headers,
'common',
'options',
'get',
'head',
'post',
'put',
'delete',
'trace',
'connect',
),
};
}

19
src/core/generateType.ts Normal file
View File

@ -0,0 +1,19 @@
import { toLowerCase } from '../helpers/utils';
import { AxiosAdapterRequestType } from './adapter';
import { AxiosRequestConfig, AxiosRequestMethodAlias } from './Axios';
export function generateType(
config: AxiosRequestConfig,
): AxiosAdapterRequestType {
let requestType: AxiosAdapterRequestType = 'request';
const method = toLowerCase<AxiosRequestMethodAlias>(config.method, 'get');
if (config.upload && method === 'post') {
requestType = 'upload';
}
if (config.download && method === 'get') {
requestType = 'download';
}
return requestType;
}

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

@ -0,0 +1,72 @@
import { isUndefined, isPlainObject } from '../helpers/is';
import { deepMerge } from '../helpers/utils';
import { AxiosRequestConfig } from './Axios';
type AxiosRequestConfigKey = keyof AxiosRequestConfig;
const onlyFromConfig2Keys: AxiosRequestConfigKey[] = [
'url',
'method',
'data',
'upload',
'download',
];
const priorityFromConfig2Keys: AxiosRequestConfigKey[] = [
'adapter',
'baseURL',
'paramsSerializer',
'transformRequest',
'transformResponse',
'errorHandler',
'cancelToken',
'dataType',
'responseType',
'timeout',
'enableHttp2',
'enableQuic',
'enableCache',
'sslVerify',
'validateStatus',
'onUploadProgress',
'onDownloadProgress',
];
const deepMergeConfigKeys: AxiosRequestConfigKey[] = ['headers', 'params'];
export function mergeConfig(
config1: AxiosRequestConfig = {},
config2: AxiosRequestConfig = {},
): AxiosRequestConfig {
const config: AxiosRequestConfig = {};
for (const key of onlyFromConfig2Keys) {
const value = config2[key];
if (!isUndefined(value)) {
config[key] = value as any;
}
}
for (const key of priorityFromConfig2Keys) {
const value1 = config1[key];
const value2 = config2[key];
if (!isUndefined(value2)) {
config[key] = value2 as any;
} else if (!isUndefined(value1)) {
config[key] = value1 as any;
}
}
for (const key of deepMergeConfigKeys) {
const value1 = config1[key];
const value2 = config2[key];
if (isPlainObject(value2)) {
config[key] = deepMerge(value1 ?? {}, value2) as any;
} else if (isPlainObject(value1)) {
config[key] = deepMerge(value1) as any;
}
}
return config;
}

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

@ -0,0 +1,99 @@
import { isFunction, isPlainObject } from '../helpers/is';
import { assert, toUpperCase } from '../helpers/utils';
import {
AxiosAdapterRequestConfig,
AxiosAdapterRequestMethod,
AxiosAdapterTask,
} from './adapter';
import {
AxiosProgressCallback,
AxiosRequestConfig,
AxiosResponse,
AxiosResponseError,
} from './Axios';
import { isCancelToken } from './cancel';
import { AxiosErrorResponse, createError } from './createError';
import { generateType } from './generateType';
function tryToggleProgressUpdate(
adapterConfig: AxiosAdapterRequestConfig,
progressUpdate?: (callback: AxiosProgressCallback) => void,
) {
if (isFunction(progressUpdate)) {
switch (adapterConfig.type) {
case 'upload':
if (isFunction(adapterConfig.onUploadProgress)) {
progressUpdate(adapterConfig.onUploadProgress);
}
break;
case 'download':
if (isFunction(adapterConfig.onDownloadProgress)) {
progressUpdate(adapterConfig.onDownloadProgress);
}
break;
default:
}
}
}
export function request<TData = unknown>(
config: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> {
return new Promise((resolve, reject) => {
assert(isFunction(config.adapter), 'adapter 需要是一个 function');
const adapterConfig: AxiosAdapterRequestConfig = {
...config,
url: config.url ?? '',
type: generateType(config),
method: toUpperCase<AxiosAdapterRequestMethod>(config.method, 'GET'),
success,
fail,
};
const adapterTask = config.adapter?.(adapterConfig) as
| AxiosAdapterTask
| undefined;
function success(response: AxiosResponse<TData>): void {
response.config = config;
response.request = adapterTask;
if (
!isFunction(config.validateStatus) ||
config.validateStatus(response.status)
) {
resolve(response);
} else {
catchError('请求失败', response);
}
}
function fail(error: AxiosResponseError): void {
error.config = config;
error.request = adapterTask;
catchError('网络错误', error);
}
function catchError(message: string, response?: AxiosErrorResponse): void {
reject(createError(message, config, adapterTask, response));
}
if (isPlainObject(adapterTask)) {
tryToggleProgressUpdate(adapterConfig, adapterTask.onProgressUpdate);
}
if (isCancelToken(config.cancelToken)) {
config.cancelToken.listener.then((reason: unknown) => {
if (isPlainObject(adapterTask)) {
tryToggleProgressUpdate(adapterConfig, adapterTask.offProgressUpdate);
if (isFunction(adapterTask.abort)) {
adapterTask.abort();
}
}
reject(reason);
});
}
});
}

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

@ -0,0 +1,33 @@
import { isArray, isUndefined } from '../helpers/is';
import {
AxiosRequestData,
AxiosRequestFormData,
AxiosResponseHeaders,
} from './Axios';
export interface AxiosTransformer {
(
data?: AxiosRequestData | AxiosRequestFormData,
headers?: AxiosResponseHeaders,
): AxiosRequestData | AxiosRequestFormData;
}
export function transformData(
data?: AxiosRequestData | AxiosRequestFormData,
headers?: AxiosResponseHeaders,
transforms?: AxiosTransformer | AxiosTransformer[],
): AxiosRequestData | AxiosRequestFormData | undefined {
if (isUndefined(transforms)) {
return data;
}
if (!isArray(transforms)) {
transforms = [transforms];
}
transforms.forEach((transform: AxiosTransformer) => {
data = transform(data, headers);
});
return data;
}

25
src/core/transformURL.ts Normal file
View File

@ -0,0 +1,25 @@
import {
buildURL,
combineURL,
dynamicInterpolation,
isAbsoluteURL,
isDynamicURL,
} from '../helpers/url';
import { AxiosRequestConfig } from './Axios';
export function transformURL(config: AxiosRequestConfig): string {
let url = config.url ?? '';
if (!isAbsoluteURL(url)) {
url = combineURL(config.baseURL, url);
}
if (isDynamicURL(url)) {
const sourceData = Object.assign({}, config.params, config.data);
url = dynamicInterpolation(url, sourceData);
}
url = buildURL(url, config.params, config.paramsSerializer);
return url;
}

35
src/defaults.ts Normal file
View File

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

37
src/helpers/is.ts Normal file
View File

@ -0,0 +1,37 @@
const _toString = Object.prototype.toString;
export function isArray<T = unknown>(value: any): value is T[] {
return Array.isArray(value);
}
export function isDate(date: any): date is Date {
return _toString.call(date) === '[object Date]';
}
export function isEmptyArray<T = unknown>(value: any): value is [] {
return isArray<T>(value) && value.length === 0;
}
export function isEmptyObject(value: any): value is object {
return isPlainObject(value) && Object.keys(value).length === 0;
}
export function isFunction<T extends () => void>(value: any): value is T {
return typeof value === 'function';
}
export function isNull(value: any): value is null {
return value === null;
}
export function isPlainObject(value: any): value is object {
return _toString.call(value) === '[object Object]';
}
export function isString(value: any): value is string {
return typeof value === 'string';
}
export function isUndefined(value: any): value is undefined {
return typeof value === 'undefined';
}

108
src/helpers/url.ts Normal file
View File

@ -0,0 +1,108 @@
import { isDate, isNull, isPlainObject, isUndefined } from './is';
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, ']');
}
export function buildURL(
url = '',
params?: unknown,
paramsSerializer = paramsSerialization,
): string {
if (!isPlainObject(params)) {
return url;
}
return generateURL(url, paramsSerializer(params));
}
const combineREG = /([^:])\/{2,}/g;
export function combineURL(baseURL = '', url: string): string {
const separator = '/';
const replaceStr = `$1${separator}`;
return `${baseURL}${separator}${url}`.replace(combineREG, replaceStr);
}
const dynamicREG = /\/?(:([a-zA-Z_$][\w-$]*))\/??/g;
export function dynamicInterpolation(
url: string,
sourceData?: unknown,
): string {
if (!isPlainObject(sourceData)) {
return url;
}
return url.replace(dynamicREG, ($1, $2, $3) =>
$1.replace($2, sourceData[$3]),
);
}
const absoluteREG = /^([a-z][a-z\d+\-.]*:)?\/\//i;
export function isAbsoluteURL(url: string): boolean {
return absoluteREG.test(url);
}
export function isDynamicURL(url: string): boolean {
dynamicREG.lastIndex = 0;
return dynamicREG.test(url);
}
function generateURL(url: string, serializedParams: string): string {
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}`;
}
function paramsSerialization(params?: AnyObject): string {
if (!isPlainObject(params)) {
return '';
}
const parts: string[] = [];
Object.keys(params).forEach((key): void => {
const value = params[key];
if (isNull(value) || isUndefined(value) || value !== value) {
return;
}
if (Array.isArray(value)) {
key += '[]';
}
const values = [].concat(value);
values.forEach((val: any): void => {
if (isPlainObject(val)) {
val = JSON.stringify(val);
} else if (isDate(val)) {
val = (val as Date).toISOString();
}
parts.push(`${encode(key)}=${encode(val)}`);
});
});
return parts.join('&');
}

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

@ -0,0 +1,70 @@
import { isPlainObject, isString } from './is';
export function deepMerge<T = unknown>(...objs: T[]): T {
const result: AnyObject = {};
objs.forEach((obj: AnyObject) =>
Object.keys(obj).forEach((key) => {
const val = obj[key];
const resultVal = result[key];
if (isPlainObject(resultVal) && isPlainObject(val)) {
result[key] = deepMerge(resultVal, val);
} else if (isPlainObject(val)) {
result[key] = deepMerge(val);
} else {
result[key] = val;
}
}),
);
return result as T;
}
export function pick<T extends AnyObject, K extends keyof T>(
obj: T,
...keys: K[]
): Pick<T, K> {
const _pick: Partial<T> = {};
keys.forEach((key: K) => (_pick[key] = obj[key]));
return _pick as Pick<T, K>;
}
export function omit<T extends AnyObject, K extends keyof T>(
obj: T,
...keys: K[]
): Omit<T, K> {
const _omit = Object.assign({}, obj);
keys.forEach((key: K) => delete _omit[key]);
return _omit;
}
export function assert(condition: boolean, msg: string) {
if (!condition) {
throwError(msg);
}
}
export function throwError(msg: string): void {
throw new Error(`[axios-miniprogram]: ${msg}`);
}
export function toLowerCase<T extends string>(value: unknown, defaultValue: T): T {
if (!isString(value)) {
value = defaultValue;
}
return value.toLowerCase() as T;
}
export function toUpperCase<T extends string>(value: unknown, defaultValue: T): T {
if (!isString(value)) {
value = defaultValue;
}
return value.toUpperCase() as T;
}

15
src/index.ts Normal file
View File

@ -0,0 +1,15 @@
import axios from './axios';
export type {
AxiosRequestConfig,
AxiosRequestFormData,
AxiosResponse,
AxiosResponseError,
} from './core/Axios';
export type {
AxiosAdapterRequestConfig,
AxiosAdapter,
AxiosPlatform,
} from './core/adapter';
export type { AxiosInstance, AxiosStatic } from './axios';
export default axios;

116
test/helpers/is.test.ts Normal file
View File

@ -0,0 +1,116 @@
import {
isArray,
isDate,
isEmptyArray,
isEmptyObject,
isFunction,
isNull,
isPlainObject,
isString,
isUndefined,
} from '../../src/helpers/is';
describe('对 src/helpers/is.ts 进行测试', () => {
it('测试 isArray() 执行结果是否符合预期', () => {
expect(isArray([0])).toBe(true);
expect(isArray([])).toBe(true);
expect(isArray({})).toBe(false);
expect(isArray(0)).toBe(false);
expect(isArray('')).toBe(false);
expect(isArray(undefined)).toBe(false);
expect(isArray(null)).toBe(false);
});
it('测试 isDate() 执行结果是否符合预期', () => {
expect(isDate(new Date())).toBe(true);
expect(isDate({})).toBe(false);
expect(isDate([])).toBe(false);
expect(isDate(0)).toBe(false);
expect(isDate('')).toBe(false);
expect(isDate(undefined)).toBe(false);
expect(isDate(null)).toBe(false);
});
it('测试 isEmptyArray() 执行结果是否符合预期', () => {
expect(isEmptyArray([])).toBe(true);
expect(isEmptyArray([0])).toBe(false);
expect(isEmptyArray({})).toBe(false);
expect(isEmptyArray(0)).toBe(false);
expect(isEmptyArray('')).toBe(false);
expect(isEmptyArray(undefined)).toBe(false);
expect(isEmptyArray(null)).toBe(false);
});
it('测试 isEmptyObject() 执行结果是否符合预期', () => {
expect(isEmptyObject({})).toBe(true);
expect(isEmptyObject({ a: 0 })).toBe(false);
expect(isEmptyObject([0])).toBe(false);
expect(isEmptyObject([])).toBe(false);
expect(isEmptyObject(0)).toBe(false);
expect(isEmptyObject('')).toBe(false);
expect(isEmptyObject(undefined)).toBe(false);
expect(isEmptyObject(null)).toBe(false);
});
it('测试 isFunction() 执行结果是否符合预期', () => {
expect(
isFunction(() => {
return;
}),
).toBe(true);
expect(
isFunction(function () {
return;
}),
).toBe(true);
expect(isFunction({})).toBe(false);
expect(isFunction([])).toBe(false);
expect(isFunction(0)).toBe(false);
expect(isFunction('')).toBe(false);
expect(isFunction(undefined)).toBe(false);
expect(isFunction(null)).toBe(false);
});
it('测试 isNull() 执行结果是否符合预期', () => {
expect(isNull(null)).toBe(true);
expect(isNull({ a: 0 })).toBe(false);
expect(isNull([0])).toBe(false);
expect(isNull([])).toBe(false);
expect(isNull(0)).toBe(false);
expect(isNull('')).toBe(false);
expect(isNull(undefined)).toBe(false);
});
it('测试 isPlainObject() 执行结果是否符合预期', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ a: 0 })).toBe(true);
expect(isPlainObject([0])).toBe(false);
expect(isPlainObject([])).toBe(false);
expect(isPlainObject(0)).toBe(false);
expect(isPlainObject('')).toBe(false);
expect(isPlainObject(undefined)).toBe(false);
expect(isPlainObject(null)).toBe(false);
});
it('测试 isString() 执行结果是否符合预期', () => {
expect(isString('')).toBe(true);
expect(isString({})).toBe(false);
expect(isString({ a: 0 })).toBe(false);
expect(isString([0])).toBe(false);
expect(isString([])).toBe(false);
expect(isString(0)).toBe(false);
expect(isString(undefined)).toBe(false);
expect(isString(null)).toBe(false);
});
it('测试 isUndefined() 执行结果是否符合预期', () => {
expect(isUndefined(undefined)).toBe(true);
expect(isUndefined('')).toBe(false);
expect(isUndefined({})).toBe(false);
expect(isUndefined({ a: 0 })).toBe(false);
expect(isUndefined([0])).toBe(false);
expect(isUndefined([])).toBe(false);
expect(isUndefined(0)).toBe(false);
expect(isUndefined(null)).toBe(false);
});
});

87
test/helpers/url.test.ts Normal file
View File

@ -0,0 +1,87 @@
import {
buildURL,
combineURL,
dynamicInterpolation,
isAbsoluteURL,
isDynamicURL,
} from '../../src/helpers/url';
describe('对 src/helpers/url.ts 进行测试', () => {
it('测试 buildURL() 执行结果是否符合预期', () => {
expect(buildURL('/api')).toBe('/api');
expect(buildURL('/api', {})).toBe('/api');
expect(buildURL('/api#id=1', {})).toBe('/api');
expect(
buildURL('/api', {
id: 1,
}),
).toBe('/api?id=1');
expect(buildURL('/api', { id: 100 }, () => 'id=1')).toBe('/api?id=1');
expect(
buildURL('/api?sid=0', {
id: 1,
}),
).toBe('/api?sid=0&id=1');
expect(buildURL('/api?sid=0', { id: 100 }, () => 'id=1')).toBe(
'/api?sid=0&id=1',
);
});
it('测试 combineURL() 执行结果是否符合预期', () => {
expect(combineURL('https://www.server.com', 'api')).toBe(
'https://www.server.com/api',
);
expect(combineURL('https://www.server.com/', '/api')).toBe(
'https://www.server.com/api',
);
expect(combineURL('https://www.server.com:8080//', '//api//')).toBe(
'https://www.server.com:8080/api/',
);
});
it('测试 dynamicInterpolation() 执行结果是否符合预期', () => {
expect(
dynamicInterpolation('https://www.server.com/api/user/:id', {
id: 1,
name: 'user',
}),
).toBe('https://www.server.com/api/user/1');
expect(
dynamicInterpolation('https://www.server.com:8080/api/user/:id', {
id: 1,
name: 'user',
}),
).toBe('https://www.server.com:8080/api/user/1');
expect(
dynamicInterpolation('https://www.server.com/api/user/:id/:name', {
id: 1,
}),
).toBe('https://www.server.com/api/user/1/undefined');
expect(
dynamicInterpolation('https://www.server.com/api/user/:id:name', {
id: 1,
}),
).toBe('https://www.server.com/api/user/1undefined');
});
it('测试 isAbsoluteURL() 执行结果是否符合预期', () => {
expect(isAbsoluteURL('')).toBe(false);
expect(isAbsoluteURL('/api')).toBe(false);
expect(isAbsoluteURL('http:')).toBe(false);
expect(isAbsoluteURL('//file')).toBe(true);
expect(isAbsoluteURL('https://www.server.com')).toBe(true);
expect(isAbsoluteURL('file://')).toBe(true);
});
it('测试 isDynamicURL() 执行结果是否符合预期', () => {
expect(isDynamicURL('')).toBe(false);
expect(isDynamicURL(':id')).toBe(true);
expect(isDynamicURL(':8080')).toBe(false);
expect(isDynamicURL('/:id')).toBe(true);
expect(isDynamicURL('/:8080')).toBe(false);
expect(isDynamicURL('https://www.server.com:8080')).toBe(false);
expect(isDynamicURL('/api')).toBe(false);
expect(isDynamicURL('/api:id')).toBe(true);
expect(isDynamicURL('/api/:id')).toBe(true);
});
});

View File

@ -0,0 +1,65 @@
import {
assert,
deepMerge,
omit,
pick,
throwError,
toLowerCase,
toUpperCase,
} from '../../src/helpers/utils';
describe('对 src/helpers/utils.ts 进行测试', () => {
it('测试 assert() 执行结果是否符合预期', () => {
expect(assert(true, '')).toBeUndefined();
expect(() => assert(false, '')).toThrow();
expect(() => assert(false, 'msg')).toThrowError('[axios-miniprogram]: msg');
});
it('测试 deepMerge() 执行结果是否符合预期', () => {
expect(deepMerge({})).toEqual({});
expect(deepMerge({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
expect(deepMerge({ a: { a: 1 } }, { a: { b: 2 } })).toEqual({
a: { a: 1, b: 2 },
});
expect(deepMerge({ a: { a: 1, b: 1 } }, { a: { a: 2, b: 2 } })).toEqual({
a: { a: 2, b: 2 },
});
expect(deepMerge({ a: { a: 1 } }, { a: 2 })).toEqual({
a: 2,
});
});
it('测试 omit() 执行结果是否符合预期', () => {
expect(omit({})).toEqual({});
expect(omit({ a: 1, b: 1 }, 'a')).toEqual({ b: 1 });
expect(omit({ a: 1, b: 1 }, 'a', 'b')).toEqual({});
});
it('测试 pick() 执行结果是否符合预期', () => {
expect(pick({})).toEqual({});
expect(pick({ a: 1, b: 1 }, 'a')).toEqual({ a: 1 });
expect(pick({ a: 1, b: 1 }, 'a', 'b')).toEqual({ a: 1, b: 1 });
});
it('测试 throwError() 执行结果是否符合预期', () => {
expect(() => throwError('')).toThrowError('[axios-miniprogram]: ');
expect(() => throwError('msg')).toThrowError('[axios-miniprogram]: msg');
expect(() => throwError(' msg ')).toThrowError(
'[axios-miniprogram]: msg ',
);
});
it('测试 toLowerCase() 执行结果是否符合预期', () => {
expect(toLowerCase('', 'GET')).toBe('');
expect(toLowerCase(undefined, 'GET')).toBe('get');
expect(toLowerCase('GET', '')).toBe('get');
expect(toLowerCase('Get', '')).toBe('get');
});
it('测试 toUpperCase() 执行结果是否符合预期', () => {
expect(toUpperCase('', 'get')).toBe('');
expect(toUpperCase(undefined, 'get')).toBe('GET');
expect(toUpperCase('get', '')).toBe('GET');
expect(toUpperCase('Get', '')).toBe('GET');
});
});

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"include": ["src", "global.d.ts"]
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"rootDir": ".",
"baseUrl": ".",
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"strict": true,
"noEmit": true
},
"include": ["src", "test", "global.d.ts"],
"exclude": ["node_modules"]
}