├── example ├── src │ ├── app.css │ ├── pages │ │ └── index │ │ │ ├── index.css │ │ │ ├── index.config.ts │ │ │ └── index.tsx │ ├── app.config.ts │ ├── app.ts │ └── index.html ├── .gitignore ├── config │ ├── dev.js │ ├── prod.js │ └── index.js ├── .editorconfig ├── babel.config.js ├── global.d.ts ├── project.config.json ├── tsconfig.json └── package.json ├── .gitignore ├── src ├── adapters │ ├── index.ts │ ├── xhr.ts │ └── taro.ts ├── helpers │ ├── index.ts │ ├── FileData.ts │ └── PostData.ts ├── env.ts ├── index.ts └── utils.ts ├── .gitattributes ├── .travis.yml ├── bili.config.ts ├── .editorconfig ├── tsconfig.json ├── LICENSE ├── patches └── axios+0.19.2.patch ├── README.md ├── package.json ├── CHANGELOG.md └── tests └── index.test.ts /example/src/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/pages/index/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/pages/index/index.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | navigationBarTitleText: '首页', 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.log 4 | *_cache 5 | *_temp 6 | dist 7 | lib 8 | coverage 9 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | deploy_versions/ 3 | .temp/ 4 | .rn_temp/ 5 | node_modules/ 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | // @index('./*', (pp, cc) => `export * from '${pp.path}'`) 2 | export * from './taro' 3 | export * from './xhr' 4 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | // @index('./*', (pp, cc) => `export * from '${pp.path}'`) 2 | export * from './FileData' 3 | export * from './PostData' 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.png binary 4 | *.jpg binary 5 | *.gif binary 6 | *.jpeg binary 7 | *.mp3 binary 8 | *.aac binary 9 | *.mp4 binary 10 | -------------------------------------------------------------------------------- /example/config/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"development"', 4 | }, 5 | defineConstants: { 6 | }, 7 | weapp: {}, 8 | h5: {}, 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/FileData.ts: -------------------------------------------------------------------------------- 1 | export class FileData { 2 | constructor(private fileContent: T) {} 3 | 4 | getFileContent(): T { 5 | return this.fileContent 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import { getTaro } from './utils' 2 | 3 | const Taro = getTaro() 4 | 5 | export const isWebLikeEnv = ( 6 | [ 7 | Taro.ENV_TYPE.WEB, 8 | Taro.ENV_TYPE.RN, 9 | ].indexOf(Taro.getEnv()) >= 0 10 | ) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 11 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - yarn 9 | script: 10 | - yarn test:cov 11 | after_success: 12 | - yarn codecov 13 | branches: 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | // babel-preset-taro 更多选项和默认值: 2 | // https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md 3 | module.exports = { 4 | presets: [ 5 | ['taro', { 6 | framework: 'react', 7 | ts: true, 8 | }], 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /example/src/app.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | pages: [ 3 | 'pages/index/index', 4 | ], 5 | window: { 6 | backgroundTextStyle: 'light', 7 | navigationBarBackgroundColor: '#fff', 8 | navigationBarTitleText: 'WeChat', 9 | navigationBarTextStyle: 'black', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { isWebLikeEnv } from './env' 3 | import { taroAdapter, xhrAdapter } from './adapters' 4 | 5 | axios.defaults.adapter = isWebLikeEnv ? xhrAdapter : taroAdapter 6 | 7 | export * from 'axios' 8 | 9 | export * from './helpers' 10 | 11 | export { axios } 12 | 13 | export default axios 14 | -------------------------------------------------------------------------------- /bili.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'bili' 2 | 3 | const config: Config = { 4 | input: 'src/index.ts', 5 | output: { 6 | target: 'browser', 7 | format: ['cjs', 'cjs-min', 'es'], 8 | dir: 'lib', 9 | sourceMap: true, 10 | }, 11 | bundleNodeModules: true, 12 | externals: [/@tarojs/], 13 | } 14 | 15 | export default config 16 | -------------------------------------------------------------------------------- /example/src/app.ts: -------------------------------------------------------------------------------- 1 | import './app.css' 2 | import { Component } from 'react' 3 | 4 | class App extends Component { 5 | componentDidMount() {} 6 | 7 | componentDidShow() {} 8 | 9 | componentDidHide() {} 10 | 11 | componentDidCatchError() {} 12 | 13 | // this.props.children 是将要会渲染的页面 14 | render() { 15 | return this.props.children 16 | } 17 | } 18 | 19 | export default App 20 | -------------------------------------------------------------------------------- /example/config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"', 4 | }, 5 | defineConstants: { 6 | }, 7 | weapp: {}, 8 | h5: { 9 | /** 10 | * 如果h5端编译后体积过大,可以使用webpack-bundle-analyzer插件对打包体积进行分析。 11 | * 参考代码如下: 12 | * webpackChain (chain) { 13 | * chain.plugin('analyzer') 14 | * .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) 15 | * } 16 | */ 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /example/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.gif'; 3 | declare module '*.jpg'; 4 | declare module '*.jpeg'; 5 | declare module '*.svg'; 6 | declare module '*.css'; 7 | declare module '*.less'; 8 | declare module '*.scss'; 9 | declare module '*.sass'; 10 | declare module '*.styl'; 11 | 12 | // @ts-ignore 13 | declare const process: { 14 | env: { 15 | TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq', 16 | [key: string]: any, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | # Minified JavaScript files shouldn't be changed 17 | [**.min.js] 18 | indent_style = ignore 19 | insert_final_newline = ignore 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "strict": true, 5 | "target": "esnext", 6 | "module": "commonjs", 7 | "declaration": true, 8 | "removeComments": false, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "outDir": "lib", 14 | "skipLibCheck": true, 15 | "newLine": "LF", 16 | "sourceMap": true 17 | }, 18 | "include": [ 19 | "src" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "lib", 24 | "dist" 25 | ], 26 | "compileOnSave": false 27 | } 28 | -------------------------------------------------------------------------------- /example/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "dist/", 3 | "projectname": "my", 4 | "description": "test", 5 | "appid": "wx15002fb034d4fb9a", 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": false, 9 | "postcss": false, 10 | "minified": false, 11 | "newFeature": true, 12 | "coverView": true, 13 | "autoAudits": false, 14 | "checkInvalidKey": true, 15 | "checkSiteMap": true, 16 | "uploadWithSourceMap": true, 17 | "babelSetting": { 18 | "ignore": [], 19 | "disablePlugins": [], 20 | "outputPath": "" 21 | } 22 | }, 23 | "compileType": "miniprogram", 24 | "simulatorType": "wechat", 25 | "simulatorPluginLibVersion": {}, 26 | "condition": {} 27 | } -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "removeComments": false, 6 | "preserveConstEnums": true, 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "noImplicitAny": false, 10 | "allowSyntheticDefaultImports": true, 11 | "outDir": "lib", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "strictNullChecks": true, 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "rootDir": ".", 18 | "jsx": "react", 19 | "jsxFactory": "React.createElement", 20 | "allowJs": true, 21 | "resolveJsonModule": true, 22 | "typeRoots": [ 23 | "node_modules/@types", 24 | "global.d.ts" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "dist" 30 | ], 31 | "compileOnSave": false 32 | } 33 | -------------------------------------------------------------------------------- /src/helpers/PostData.ts: -------------------------------------------------------------------------------- 1 | import { FileData } from './FileData' 2 | import { isObject } from '../utils' 3 | 4 | export class PostData> { 5 | constructor(private postData: T) {} 6 | 7 | getParsedPostData() { 8 | const postData = this.postData 9 | const parsedPostData: Record<'normalData' | 'fileData', T> = { 10 | normalData: {} as any, 11 | fileData: {} as any, 12 | } 13 | if (isObject(postData)) { 14 | Object.keys(postData).forEach(key => { 15 | if (postData[key] && postData[key] instanceof FileData) { 16 | (parsedPostData.fileData as any)[key] = (postData[key] as FileData).getFileContent() 17 | } else { 18 | (parsedPostData.normalData as any)[key] = postData[key] 19 | } 20 | }) 21 | } 22 | return parsedPostData 23 | } 24 | 25 | toString() { 26 | return `[object ${PostData.name}]` 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as rawTaro from '@tarojs/taro' 2 | // @ts-ignore 3 | import utils from 'axios/lib/utils' 4 | 5 | const { isString, isObject, forEach, merge } = utils as { 6 | isString(value: any): value is string, 7 | isObject(value: any): value is Record, 8 | forEach>(obj: T, fn: (value: T[keyof T], key: keyof T, obj: T) => void): void, 9 | merge>(...args: T[]): T, 10 | } 11 | 12 | function objectToQueryString(obj: Record) { 13 | const result: string[] = [] 14 | forEach(obj, (value, key) => { 15 | result.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`) 16 | }) 17 | return result.join('&') 18 | } 19 | 20 | function getTaro(): typeof rawTaro { 21 | const Taro = require('@tarojs/taro') as any 22 | 23 | return Taro && (Taro as any).default || Taro 24 | } 25 | 26 | export { isString, isObject, forEach, merge, objectToQueryString, getTaro } 27 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Jay Fong (https://github.com/fjc0k) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | projectName: 'my', 3 | date: '2020-3-19', 4 | designWidth: 750, 5 | deviceRatio: { 6 | 640: 2.34 / 2, 7 | 750: 1, 8 | 828: 1.81 / 2, 9 | }, 10 | sourceRoot: 'src', 11 | outputRoot: 'dist', 12 | plugins: [], 13 | defineConstants: { 14 | }, 15 | copy: { 16 | patterns: [ 17 | ], 18 | options: { 19 | }, 20 | }, 21 | framework: 'react', 22 | mini: { 23 | postcss: { 24 | pxtransform: { 25 | enable: true, 26 | config: { 27 | 28 | }, 29 | }, 30 | url: { 31 | enable: true, 32 | config: { 33 | limit: 1024, // 设定转换尺寸上限 34 | }, 35 | }, 36 | cssModules: { 37 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 38 | config: { 39 | namingPattern: 'module', // 转换模式,取值为 global/module 40 | generateScopedName: '[name]__[local]___[hash:base64:5]', 41 | }, 42 | }, 43 | }, 44 | }, 45 | h5: { 46 | publicPath: '/', 47 | staticDirectory: 'static', 48 | postcss: { 49 | autoprefixer: { 50 | enable: true, 51 | config: { 52 | }, 53 | }, 54 | cssModules: { 55 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 56 | config: { 57 | namingPattern: 'module', // 转换模式,取值为 global/module 58 | generateScopedName: '[name]__[local]___[hash:base64:5]', 59 | }, 60 | }, 61 | }, 62 | }, 63 | } 64 | 65 | module.exports = function (merge) { 66 | if (process.env.NODE_ENV === 'development') { 67 | return merge({}, config, require('./dev')) 68 | } 69 | return merge({}, config, require('./prod')) 70 | } 71 | -------------------------------------------------------------------------------- /patches/axios+0.19.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/axios/lib/defaults.js b/node_modules/axios/lib/defaults.js 2 | index 23e1cbd..0b91da2 100644 3 | --- a/node_modules/axios/lib/defaults.js 4 | +++ b/node_modules/axios/lib/defaults.js 5 | @@ -36,7 +36,9 @@ var defaults = { 6 | utils.isBuffer(data) || 7 | utils.isStream(data) || 8 | utils.isFile(data) || 9 | - utils.isBlob(data) 10 | + utils.isBlob(data) || 11 | + // 放行 PostData 12 | + (data !== null && typeof data === 'object' && data.toString() === '[object PostData]') 13 | ) { 14 | return data; 15 | } 16 | diff --git a/node_modules/axios/lib/utils.js b/node_modules/axios/lib/utils.js 17 | index e028c67..ce878c5 100644 18 | --- a/node_modules/axios/lib/utils.js 19 | +++ b/node_modules/axios/lib/utils.js 20 | @@ -193,12 +193,18 @@ function trim(str) { 21 | function isStandardBrowserEnv() { 22 | if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' || 23 | navigator.product === 'NativeScript' || 24 | - navigator.product === 'NS')) { 25 | + navigator.product === 'NS' || 26 | + // 兼容 Taro 3 27 | + // ref: https://github.com/NervJS/taro/commit/7349f716111accb6a39c72ed0f1e6cbc166fa32b 28 | + navigator.product === 'Taro')) { 29 | return false; 30 | } 31 | return ( 32 | typeof window !== 'undefined' && 33 | - typeof document !== 'undefined' 34 | + typeof document !== 'undefined' && 35 | + // 兼容 Taro 3 36 | + // ref: https://github.com/NervJS/taro/issues/5741#issuecomment-601102518 37 | + !!document.scripts 38 | ); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/adapters/xhr.ts: -------------------------------------------------------------------------------- 1 | import { AxiosAdapter } from 'axios' 2 | import { forEach, isString, objectToQueryString } from '../utils' 3 | import { PostData } from '../helpers' 4 | // @ts-ignore 5 | import nativeXhrAdapter from 'axios/lib/adapters/xhr' 6 | 7 | export const xhrAdapter: AxiosAdapter = config => { 8 | return new Promise(resolve => { 9 | // 适配 PostData 10 | if (config.data && config.data instanceof PostData) { 11 | const { normalData, fileData } = config.data.getParsedPostData() 12 | const hasFileData = Object.keys(fileData).length > 0 13 | if (hasFileData) { 14 | const formData = new FormData() 15 | forEach( 16 | normalData, 17 | (value, key) => { 18 | formData.set(key as any, value) 19 | }, 20 | ) 21 | Promise.all( 22 | Object.keys(fileData).map(key => { 23 | return new Promise(resolve => { 24 | const fileContent = fileData[key] 25 | 26 | // 兼容 blob 地址 27 | if (isString(fileContent) && fileContent.indexOf('blob:') === 0) { 28 | const xhr = new XMLHttpRequest() 29 | xhr.open('GET', fileContent) 30 | xhr.responseType = 'blob' 31 | xhr.onload = () => { 32 | resolve(xhr.response) 33 | } 34 | xhr.send() 35 | } else { 36 | resolve(fileContent) 37 | } 38 | }).then(fileContent => formData.set(key, fileContent as any)) 39 | }), 40 | ).then(() => { 41 | config.data = formData 42 | resolve() 43 | }) 44 | } else { 45 | config.data = objectToQueryString(normalData) 46 | config.headers['Content-Type'] = 'application/x-www-form-urlencoded' 47 | resolve() 48 | } 49 | } else { 50 | resolve() 51 | } 52 | }).then(() => nativeXhrAdapter(config)) 53 | } 54 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "test", 6 | "templateInfo": { 7 | "name": "default", 8 | "typescript": true, 9 | "css": "none" 10 | }, 11 | "scripts": { 12 | "build:weapp": "taro build --type weapp", 13 | "build:swan": "taro build --type swan", 14 | "build:alipay": "taro build --type alipay", 15 | "build:tt": "taro build --type tt", 16 | "build:h5": "taro build --type h5", 17 | "build:rn": "taro build --type rn", 18 | "build:qq": "taro build --type qq", 19 | "build:quickapp": "taro build --type quickapp", 20 | "dev:weapp": "npm run build:weapp -- --watch", 21 | "dev:swan": "npm run build:swan -- --watch", 22 | "dev:alipay": "npm run build:alipay -- --watch", 23 | "dev:tt": "npm run build:tt -- --watch", 24 | "dev:h5": "npm run build:h5 -- --watch", 25 | "dev:rn": "npm run build:rn -- --watch", 26 | "dev:qq": "npm run build:qq -- --watch", 27 | "dev:quickapp": "npm run build:quickapp -- --watch" 28 | }, 29 | "browserslist": [ 30 | "last 3 versions", 31 | "Android >= 4.1", 32 | "ios >= 8" 33 | ], 34 | "author": "", 35 | "dependencies": { 36 | "@babel/runtime": "^7.7.7", 37 | "@tarojs/components": "3.0.0-beta.4", 38 | "@tarojs/react": "3.0.0-beta.4", 39 | "@tarojs/runtime": "3.0.0-beta.4", 40 | "@tarojs/taro": "3.0.0-beta.4", 41 | "react": "^16.10.0", 42 | "react-dom": "^16.10.0", 43 | "taro-axios": "^1.1.0" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.8.0", 47 | "@tarojs/cli": "^3.0.0-beta.4", 48 | "@tarojs/mini-runner": "3.0.0-beta.4", 49 | "@tarojs/webpack-runner": "3.0.0-beta.4", 50 | "@types/react": "^16.0.0", 51 | "@types/webpack-env": "^1.13.6", 52 | "@typescript-eslint/eslint-plugin": "^2.x", 53 | "@typescript-eslint/parser": "^2.x", 54 | "babel-preset-taro": "3.0.0-beta.4", 55 | "eslint": "^6.8.0", 56 | "eslint-config-taro": "3.0.0-beta.4", 57 | "eslint-plugin-import": "^2.12.0", 58 | "eslint-plugin-react": "^7.8.2", 59 | "eslint-plugin-react-hooks": "^1.6.1", 60 | "stylelint": "9.3.0", 61 | "typescript": "^3.7.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # taro-axios NPM Version Build Status Coverage Status License 2 | 3 | 在 [Taro](https://github.com/NervJS/taro) 中使用 [axios](https://github.com/axios/axios)。 4 | 5 | ## 源起 6 | 7 | 因为 `Taro` 不支持解析 `package.json` 里的 `browser` 属性,导致所有使用了该特性的包都可能无法在 `Taro` 里正常运行。不幸的是,`axios` 就是其中之一。 8 | 9 | 于是,`taro-axios` 预先解析了 `axios` 包中的 `browser` 属性并提供了 `Taro` 版的请求适配器后,将之打包出了一个 `Taro` 可用的版本。 10 | 11 | 也就是说,`taro-axios` 只是 `axios` 的 `Taro` 重制版,并非是为 `Taro` 仿写了一个 `axios`。`axios` 提供什么,`taro-axios` 也就提供什么。 12 | 13 | ## 特性 14 | 15 | - 使用 TypeScript 编写,类型友好 16 | - 基于 Taro 适配器,天然支持多端 17 | - 支持 API 一致的多端上传文件 18 | 19 | ## 安装 20 | 21 | ### Taro 3 22 | 23 | ```bash 24 | # yarn 25 | yarn add taro-axios 26 | 27 | # 或, npm 28 | npm i taro-axios --save 29 | ``` 30 | 31 | ### Taro 1、Taro 2 32 | 33 | 34 | ```bash 35 | # yarn 36 | yarn add taro-axios@0.7.0 37 | 38 | # 或, npm 39 | npm i taro-axios@0.7.0 --save 40 | ``` 41 | 42 | ## 使用 43 | 44 | 使用方法同 [axios](https://github.com/axios/axios#axios)。 45 | 46 | 只不过你得这样引入 `axios`: 47 | 48 | ```ts 49 | import { axios } from 'taro-axios' 50 | // 自版本 0.7.0 起你也可以这样引入: 51 | // import axios from 'taro-axios' 52 | 53 | axios 54 | .get('https://jsonplaceholder.typicode.com/todos/1') 55 | .then(res => { 56 | console.log(res.data) 57 | }) 58 | ``` 59 | 60 | ## 上传文件 61 | 62 | 为了支持多端上传文件,我们得引入 `PostData` 和 `FileData` 两个类,示例: 63 | 64 | ```ts 65 | import { axios, PostData, FileData } from 'taro-axios' 66 | 67 | async function uploadImage() { 68 | const { tempFilePaths } = await Taro.chooseImage({ count: 1 }) 69 | Taro.showLoading({ title: '图片上传中...' }) 70 | const res = await axios.post( 71 | 'https://sm.ms/api/upload', 72 | new PostData({ 73 | smfile: new FileData(tempFilePaths[0]), 74 | ssl: true, 75 | format: 'json', 76 | }), 77 | ) 78 | Taro.hideLoading() 79 | Taro.showModal({ 80 | title: '返回结果', 81 | content: JSON.stringify(res.data), 82 | }) 83 | } 84 | ``` 85 | 86 | ## 许可 87 | 88 | MIT © Jay Fong 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taro-axios", 3 | "version": "1.1.1", 4 | "description": "在 Taro 中使用 axios。", 5 | "main": "lib/index.js", 6 | "module": "lib/index.esm.js", 7 | "typings": "lib/index.d.ts", 8 | "author": { 9 | "name": "Jay Fong", 10 | "email": "fjc0kb@gmail.com", 11 | "url": "https://github.com/fjc0k" 12 | }, 13 | "license": "MIT", 14 | "homepage": "https://github.com/fjc0k/taro-axios", 15 | "sideEffects": false, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:fjc0k/taro-axios.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/fjc0k/taro-axios/issues" 22 | }, 23 | "keywords": [ 24 | "taro", 25 | "axios", 26 | "taro-axios" 27 | ], 28 | "files": [ 29 | "lib" 30 | ], 31 | "scripts": { 32 | "build": "bili", 33 | "lint:es": "eslint --fix src/", 34 | "test": "jest", 35 | "test:cov": "jest --coverage", 36 | "release": "standard-version --commit-all --no-verify && git push --follow-tags origin master && yarn build && clean-publish --files example --fields scripts", 37 | "patch:axios": "patch-package axios", 38 | "postinstall": "patch-package" 39 | }, 40 | "eslintConfig": { 41 | "root": true, 42 | "extends": "io" 43 | }, 44 | "eslintIgnore": [ 45 | "lib", 46 | "dist", 47 | "node_modules", 48 | "__snapshots__", 49 | "axios" 50 | ], 51 | "commitlint": { 52 | "extends": [ 53 | "io" 54 | ] 55 | }, 56 | "lint-staged": { 57 | "*.{js,jsx,ts,tsx}": [ 58 | "eslint --fix", 59 | "git add" 60 | ] 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 65 | "pre-commit": "lint-staged" 66 | } 67 | }, 68 | "jest": { 69 | "transform": { 70 | "^.+\\.tsx?$": "ts-jest" 71 | }, 72 | "collectCoverageFrom": [ 73 | "/src/**/*.ts" 74 | ], 75 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 76 | "moduleFileExtensions": [ 77 | "ts", 78 | "tsx", 79 | "js", 80 | "jsx", 81 | "json" 82 | ] 83 | }, 84 | "devDependencies": { 85 | "@commitlint/cli": "^7.5.2", 86 | "@tarojs/taro": "^1.2.26", 87 | "@tarojs/taro-h5": "^1.2.26", 88 | "@types/jest": "^24.0.11", 89 | "@types/node": "^11.11.3", 90 | "@types/qs": "^6.5.3", 91 | "bili": "^4.8.0", 92 | "clean-publish": "^1.1.6", 93 | "codecov": "^3.3.0", 94 | "commitlint-config-io": "^0.3.0", 95 | "cp-cli": "^2.0.0", 96 | "eslint": "^5.15.0", 97 | "eslint-config-io": "^0.5.1", 98 | "husky": "^1.3.1", 99 | "jest": "^24.7.1", 100 | "lint-staged": "^8.1.5", 101 | "nervjs": "^1.3.13", 102 | "patch-package": "^6.2.1", 103 | "qs": "^6.7.0", 104 | "rollup-plugin-typescript2": "^0.20.1", 105 | "standard-version": "^4.4.0", 106 | "ts-jest": "^24.0.2", 107 | "ts-node": "^8.0.3", 108 | "typescript": "^3.4.5", 109 | "vtils": "^1.3.0" 110 | }, 111 | "peerDependencies": { 112 | "@tarojs/taro": "^3" 113 | }, 114 | "dependencies": { 115 | "axios": "0.19.2" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /example/src/pages/index/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | import React, { Component } from 'react' 3 | import Taro from '@tarojs/taro' 4 | // @ts-ignore 5 | import { axios, FileData, PostData } from '../../../../src' 6 | import { Button, Text, View } from '@tarojs/components' 7 | 8 | const axiosWithBaseUrl = axios.create({ 9 | baseURL: 'https://jsonplaceholder.typicode.com', 10 | }) 11 | 12 | export default class Index extends Component { 13 | componentWillMount() { } 14 | 15 | componentDidMount() { } 16 | 17 | componentWillUnmount() { } 18 | 19 | componentDidShow() { } 20 | 21 | componentDidHide() { } 22 | 23 | render() { 24 | return ( 25 | 26 | {/* GET */} 27 | GET 28 | 46 | 64 | 65 | {/* POST */} 66 | POST 67 | 83 | 97 | 111 | 129 | 130 | ) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [1.1.1](https://github.com/fjc0k/taro-axios/compare/v1.1.0...v1.1.1) (2020-04-24) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **taro-adapter:** 完整 url 对齐 axios 实现 (close: [#21](https://github.com/fjc0k/taro-axios/issues/21)) ([4b67cc0](https://github.com/fjc0k/taro-axios/commit/4b67cc0)) 12 | 13 | 14 | 15 | 16 | # [1.1.0](https://github.com/fjc0k/taro-axios/compare/v1.0.2...v1.1.0) (2020-03-24) 17 | 18 | 19 | ### Features 20 | 21 | * 升级 axios 版本 ([35e616d](https://github.com/fjc0k/taro-axios/commit/35e616d)) 22 | 23 | 24 | 25 | 26 | ## [1.0.2](https://github.com/fjc0k/taro-axios/compare/v1.0.1...v1.0.2) (2020-03-24) 27 | 28 | 29 | 30 | 31 | ## [1.0.1](https://github.com/fjc0k/taro-axios/compare/v1.0.0...v1.0.1) (2020-03-20) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * 发布时去除 pkg.scripts (close: [#18](https://github.com/fjc0k/taro-axios/issues/18)) ([63ec65e](https://github.com/fjc0k/taro-axios/commit/63ec65e)) 37 | 38 | 39 | 40 | 41 | # [1.0.0](https://github.com/fjc0k/taro-axios/compare/v0.7.0...v1.0.0) (2020-03-19) 42 | 43 | 44 | ### Features 45 | 46 | * 支持 Taro 3 ([a576c58](https://github.com/fjc0k/taro-axios/commit/a576c58)) 47 | 48 | 49 | 50 | 51 | # [0.7.0](https://github.com/fjc0k/taro-axios/compare/v0.6.0...v0.7.0) (2019-11-06) 52 | 53 | 54 | ### Features 55 | 56 | * 默认导出 axios ([f0c6dd1](https://github.com/fjc0k/taro-axios/commit/f0c6dd1)) 57 | 58 | 59 | 60 | 61 | # [0.6.0](https://github.com/fjc0k/taro-axios/compare/v0.5.3...v0.6.0) (2019-10-30) 62 | 63 | 64 | ### Features 65 | 66 | * **taro-adapter:** 支持 timeout 特性 ([7cf1082](https://github.com/fjc0k/taro-axios/commit/7cf1082)) 67 | 68 | 69 | 70 | 71 | ## [0.5.3](https://github.com/fjc0k/taro-axios/compare/v0.5.2...v0.5.3) (2019-09-28) 72 | 73 | 74 | 75 | 76 | ## [0.5.2](https://github.com/fjc0k/taro-axios/compare/v0.5.1...v0.5.2) (2019-09-28) 77 | 78 | 79 | 80 | 81 | ## [0.5.1](https:///github.com/fjc0k/taro-axios/compare/v0.5.0...v0.5.1) (2019-08-16) 82 | 83 | 84 | 85 | 86 | # [0.5.0](https:///github.com/fjc0k/taro-axios/compare/v0.4.1...v0.5.0) (2019-08-09) 87 | 88 | 89 | ### Features 90 | 91 | * 打包新增 cjs-min 格式和 source map ([32f2694](https:///github.com/fjc0k/taro-axios/commits/32f2694)) 92 | 93 | 94 | 95 | 96 | ## [0.4.1](https:///github.com/fjc0k/taro-axios/compare/v0.4.0...v0.4.1) (2019-07-30) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * **taro-adapter:** 因状态码导致的报错应将请求结果转发给 axios 处理 ([5987c76](https:///github.com/fjc0k/taro-axios/commits/5987c76)) 102 | 103 | 104 | 105 | 106 | # [0.4.0](https:///github.com/fjc0k/taro-axios/compare/v0.3.4...v0.4.0) (2019-07-11) 107 | 108 | 109 | ### Features 110 | 111 | * 优化 H5 下 Taro 的获取 ([7085460](https:///github.com/fjc0k/taro-axios/commits/7085460)) 112 | 113 | 114 | 115 | 116 | ## [0.3.4](https:///github.com/fjc0k/taro-axios/compare/v0.3.3...v0.3.4) (2019-06-28) 117 | 118 | 119 | 120 | 121 | ## [0.3.3](https:///github.com/fjc0k/taro-axios/compare/v0.3.2...v0.3.3) (2019-06-28) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * 确保 Taro 被正确导入 (close: [#4](https:///github.com/fjc0k/taro-axios/issues/4)) ([8e8da85](https:///github.com/fjc0k/taro-axios/commits/8e8da85)) 127 | 128 | 129 | 130 | 131 | ## [0.3.2](https:///github.com/fjc0k/taro-axios/compare/v0.3.1...v0.3.2) (2019-06-07) 132 | 133 | 134 | ### Bug Fixes 135 | 136 | * 添加 getTaro 获取不同平台下的 Taro 实现 (close: [#3](https:///github.com/fjc0k/taro-axios/issues/3)) ([417e413](https:///github.com/fjc0k/taro-axios/commits/417e413)) 137 | 138 | 139 | 140 | 141 | ## [0.3.1](https:///github.com/fjc0k/taro-axios/compare/v0.3.0...v0.3.1) (2019-05-30) 142 | 143 | 144 | ### Bug Fixes 145 | 146 | * 升级 axios 解决安全提示 ([f3e2058](https:///github.com/fjc0k/taro-axios/commits/f3e2058)) 147 | 148 | 149 | 150 | 151 | # [0.3.0](https:///github.com/fjc0k/taro-axios/compare/v0.2.1...v0.3.0) (2019-05-20) 152 | 153 | 154 | ### Features 155 | 156 | * 支持多端文件上传 ([e11bdf0](https:///github.com/fjc0k/taro-axios/commits/e11bdf0)) 157 | 158 | 159 | 160 | 161 | ## [0.2.1](https:///github.com/fjc0k/taro-axios/compare/v0.2.0...v0.2.1) (2019-05-18) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * **scripts:** 需先打包再发布 ([03a2816](https:///github.com/fjc0k/taro-axios/commits/03a2816)) 167 | 168 | 169 | 170 | 171 | # [0.2.0](https:///github.com/fjc0k/taro-axios/compare/v0.1.1...v0.2.0) (2019-05-18) 172 | 173 | 174 | ### Features 175 | 176 | * 支持文件上传 ([7de508e](https:///github.com/fjc0k/taro-axios/commits/7de508e)) 177 | 178 | 179 | 180 | 181 | ## [0.1.1](https:///github.com/fjc0k/taro-axios/compare/v0.1.0...v0.1.1) (2019-05-18) 182 | 183 | 184 | 185 | 186 | # 0.1.0 (2019-05-17) 187 | 188 | 189 | ### Features 190 | 191 | * 项目首次提交 ([9c8ef38](https:///github.com/fjc0k/taro-axios/commits/9c8ef38)) 192 | -------------------------------------------------------------------------------- /src/adapters/taro.ts: -------------------------------------------------------------------------------- 1 | import { AxiosAdapter, AxiosResponse } from 'axios' 2 | import { getTaro, isObject, isString, merge } from '../utils' 3 | import { PostData } from '../helpers' 4 | // @ts-ignore 5 | import createError from 'axios/lib/core/createError' 6 | // @ts-ignore 7 | import buildFullPath from 'axios/lib/core/buildFullPath' 8 | // @ts-ignore 9 | import buildUrl from 'axios/lib/helpers/buildURL' 10 | // @ts-ignore 11 | import settle from 'axios/lib/core/settle' 12 | 13 | const Taro = getTaro() 14 | 15 | export const taroAdapter: AxiosAdapter = config => { 16 | return new Promise((resolve, reject) => { 17 | const requestMethod: string = (isString(config.method) ? config.method : 'GET').toUpperCase() 18 | const requestUrl: string = buildUrl(buildFullPath(config.baseURL, config.url), config.params, config.paramsSerializer) 19 | const requestHeaders: Record = isObject(config.headers) ? config.headers : {} 20 | 21 | // 请求数据 22 | let requestData: any = config.data 23 | 24 | // 请求任务 25 | let requestTask: Promise | null = null 26 | 27 | // 中断请求任务 28 | let abortRequestTask: (() => void) | null = null 29 | 30 | // 文件上传请求 31 | if (requestData && requestData instanceof PostData) { 32 | const { normalData, fileData } = requestData.getParsedPostData() 33 | const hasFileData = Object.keys(fileData).length > 0 34 | if (hasFileData) { 35 | const fileName = Object.keys(fileData)[0] 36 | const filePath = fileData[fileName] 37 | const request = Taro.uploadFile({ 38 | url: requestUrl, 39 | header: requestHeaders, 40 | name: fileName, 41 | filePath: filePath, 42 | formData: normalData, 43 | }) 44 | abortRequestTask = request.abort 45 | if (typeof config.onUploadProgress === 'function') { 46 | request.progress(e => { 47 | config.onUploadProgress!( 48 | merge( 49 | e, 50 | // 兼容 XMLHttpRequest.onprogress 的数据结构 51 | { 52 | total: e.totalBytesExpectedToSend, 53 | loaded: e.totalBytesSent, 54 | } as any, 55 | ), 56 | ) 57 | }) 58 | } 59 | requestTask = request.then(res => { 60 | let data = res.data 61 | if (config.responseType === 'json') { 62 | try { 63 | data = JSON.parse(data) 64 | } catch (e) {} 65 | } 66 | return { 67 | data: data, 68 | status: res.statusCode, 69 | statusText: '', 70 | headers: {}, 71 | config: config, 72 | request: request, 73 | } 74 | }) 75 | } else { 76 | requestData = normalData 77 | requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded' 78 | } 79 | } 80 | 81 | // 普通请求 82 | if (!requestTask) { 83 | const request = Taro.request({ 84 | method: requestMethod as any, 85 | url: requestUrl, 86 | header: requestHeaders, 87 | // 请求数据只在 POST, PUT, PATCH 时设置 88 | data: requestMethod === 'POST' || requestMethod === 'PUT' || requestMethod === 'PATCH' ? requestData : '', 89 | // 响应的内容只能是 arraybuffer 或 text 90 | responseType: config.responseType === 'arraybuffer' ? 'arraybuffer' : 'text', 91 | // 响应数据的类型只能是 json 或 其他 92 | dataType: config.responseType === 'json' ? 'json' : config.responseType, 93 | }) 94 | abortRequestTask = (request as any).abort 95 | requestTask = request.then(res => { 96 | return { 97 | data: res.data, 98 | status: res.statusCode, 99 | statusText: '', 100 | headers: res.header, 101 | config: config, 102 | request: request, 103 | } 104 | }) 105 | } 106 | 107 | // 支持超时处理 108 | let timer: any = null 109 | if (config.timeout) { 110 | timer = setTimeout( 111 | () => { 112 | abortRequestTask && abortRequestTask() 113 | // ref: https://github.com/axios/axios/blob/master/lib/adapters/xhr.js#L90 114 | const timeoutErrorMessage = `timeout of ${config.timeout}ms exceeded` 115 | reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', requestTask)) 116 | }, 117 | config.timeout, 118 | ) 119 | } 120 | 121 | // 请求任务结果处理 122 | requestTask 123 | .then(response => { 124 | timer && clearTimeout(timer) 125 | settle(resolve, reject, response) 126 | }) 127 | .catch(response => { 128 | timer && clearTimeout(timer) 129 | // 如果存在状态码,说明请求服务器成功,将结果转发给 axios 处理 130 | if (response && typeof response === 'object' && (response.status != null || response.statusCode != null)) { 131 | settle(resolve, reject, { 132 | data: response.data, 133 | status: response.status != null ? response.status : response.statusCode, 134 | statusText: '', 135 | headers: response.header || response.headers || {}, 136 | config: config, 137 | request: requestTask, 138 | }) 139 | } else { 140 | const error = createError('Network Error', config, undefined, requestTask) 141 | reject(error) 142 | } 143 | }) 144 | 145 | // 支持取消请求任务 146 | if (config.cancelToken) { 147 | config.cancelToken.promise.then(cancel => { 148 | timer && clearTimeout(timer) 149 | abortRequestTask && abortRequestTask() 150 | reject(cancel) 151 | }) 152 | } 153 | }) 154 | } 155 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http' 2 | import * as qs from 'qs' 3 | import * as Taro from '@tarojs/taro-h5' 4 | import { isFunction, isPlainObject, wait } from 'vtils' 5 | 6 | process.env.TARO_ENV = 'h5' 7 | 8 | const withAxiosList: Array<() => Promise> = [ 9 | async function WebOrRN() { 10 | jest.resetModules() 11 | jest.mock( 12 | '@tarojs/taro', 13 | () => ({ 14 | ...Taro, 15 | default: undefined, 16 | getEnv: () => Taro.ENV_TYPE.WEB, 17 | }), 18 | ) 19 | return import('../src') 20 | }, 21 | async function MiniProgram() { 22 | jest.resetModules() 23 | jest.mock( 24 | '@tarojs/taro', 25 | () => ({ 26 | ...Taro, 27 | default: undefined, 28 | getEnv: () => Taro.ENV_TYPE.WEAPP, 29 | }), 30 | ) 31 | return import('../src') 32 | }, 33 | ] 34 | 35 | let port = 4444 36 | 37 | async function withServer({ 38 | statusCode = 200, 39 | response, 40 | }: { 41 | statusCode?: number, 42 | response: ((req: http.IncomingMessage) => any) | Record, 43 | }) { 44 | const server = http 45 | .createServer(async (req, res) => { 46 | res.writeHead(statusCode, { 47 | 'Content-Type': 'application/json', 48 | 'Access-Control-Allow-Origin': '*', 49 | }) 50 | response = isFunction(response) ? await (response as any)(req) : response 51 | res.end(isPlainObject(response) ? JSON.stringify(response) : response) 52 | }) 53 | .listen(++port) 54 | 55 | return Promise.resolve({ 56 | url: `http://localhost:${port}`, 57 | closeServer: () => server.close(), 58 | }) 59 | } 60 | 61 | describe('多端适配', () => { 62 | [Taro.ENV_TYPE.WEB, Taro.ENV_TYPE.RN].forEach(envType => { 63 | test(`${envType} 环境下,使用 xhr 适配器`, async () => { 64 | jest.resetModules() 65 | jest.mock( 66 | '@tarojs/taro', 67 | () => ({ 68 | ...Taro, 69 | default: undefined, 70 | getEnv: () => envType, 71 | }), 72 | ) 73 | const { axios } = await import('../src') 74 | const { xhrAdapter } = await import('../src/adapters') 75 | expect(axios.defaults.adapter).toBe(xhrAdapter) 76 | jest.restoreAllMocks() 77 | }) 78 | }) 79 | 80 | test('小程序环境下,使用 taro 适配器', async () => { 81 | jest.resetModules() 82 | jest.mock( 83 | '@tarojs/taro', 84 | () => ({ 85 | ...Taro, 86 | default: undefined, 87 | getEnv: () => Taro.ENV_TYPE.WEAPP, 88 | }), 89 | ) 90 | const { axios } = await import('../src') 91 | const { taroAdapter } = await import('../src/adapters') 92 | expect(axios.defaults.adapter).toBe(taroAdapter) 93 | jest.restoreAllMocks() 94 | }) 95 | }) 96 | 97 | withAxiosList.forEach(withAxios => { 98 | describe(`${withAxios.name} - 特性支持`, () => { 99 | test('支持中断请求', async () => { 100 | const { axios } = await withAxios() 101 | const { url, closeServer } = await withServer({ 102 | response: { 103 | success: true, 104 | }, 105 | }) 106 | try { 107 | await axios.get(url, { 108 | cancelToken: new axios.CancelToken(cancel => { 109 | wait(0).then(() => cancel()) 110 | }), 111 | }) 112 | } catch (err) { 113 | expect(axios.isCancel(err)).toBeTruthy() 114 | } 115 | closeServer() 116 | }) 117 | 118 | test('异常抛出 - 服务端错误', async () => { 119 | const { axios } = await withAxios() 120 | const { url, closeServer } = await withServer({ 121 | statusCode: 500, 122 | response: { 123 | success: true, 124 | }, 125 | }) 126 | try { 127 | await axios.get(url) 128 | } catch (err) { 129 | expect(err.message).toBe('Request failed with status code 500') 130 | } 131 | closeServer() 132 | }) 133 | 134 | test('异常抛出 - 服务端错误', async () => { 135 | const { axios } = await withAxios() 136 | try { 137 | console.error = jest.fn() 138 | await axios.get('http://localh0st') 139 | } catch (err) { 140 | expect(err.message).toBe('Network Error') 141 | } 142 | }) 143 | 144 | test('超时处理', async () => { 145 | const { axios } = await withAxios() 146 | const { url, closeServer } = await withServer({ 147 | statusCode: 200, 148 | response: async () => { 149 | await wait(1500) 150 | return { success: true } 151 | }, 152 | }) 153 | try { 154 | await axios.get(url, { timeout: 1000 }) 155 | } catch (err) { 156 | expect(err.message).toBe('timeout of 1000ms exceeded') 157 | } 158 | const res = await axios.get(url, { timeout: 2000 }) 159 | expect(res.data).toEqual({ success: true }) 160 | closeServer() 161 | }) 162 | }) 163 | 164 | describe(`${withAxios.name} - GET`, () => { 165 | test('GET 正常', async () => { 166 | const { axios } = await withAxios() 167 | const { url, closeServer } = await withServer({ 168 | statusCode: 200, 169 | response: req => ({ 170 | method: req.method, 171 | success: true, 172 | }), 173 | }) 174 | const res = await axios.get(url) 175 | expect(res.data).toMatchObject({ 176 | method: 'GET', 177 | success: true, 178 | }) 179 | closeServer() 180 | }) 181 | 182 | test('GET 发送请求串数据正常', async () => { 183 | const { axios } = await withAxios() 184 | const { url, closeServer } = await withServer({ 185 | statusCode: 200, 186 | response: req => ({ 187 | method: req.method, 188 | query: qs.parse(req.url!.split('?')[1]), 189 | success: true, 190 | }), 191 | }) 192 | const res = await axios.get(url, { 193 | params: { 194 | ok: 'ok', 195 | age: 12, 196 | }, 197 | }) 198 | expect(res.data).toMatchObject({ 199 | method: 'GET', 200 | query: { 201 | ok: 'ok', 202 | age: '12', 203 | }, 204 | success: true, 205 | }) 206 | closeServer() 207 | }) 208 | }) 209 | 210 | describe(`${withAxios.name} - POST`, () => { 211 | test('POST 正常', async () => { 212 | const { axios } = await withAxios() 213 | const { url, closeServer } = await withServer({ 214 | statusCode: 200, 215 | response: req => ({ 216 | method: req.method, 217 | success: true, 218 | }), 219 | }) 220 | const res = await axios.post(url) 221 | expect(res.data).toMatchObject({ 222 | method: 'POST', 223 | success: true, 224 | }) 225 | closeServer() 226 | }) 227 | 228 | test('POST 发送 JSON 数据正常', async () => { 229 | const { axios } = await withAxios() 230 | const { url, closeServer } = await withServer({ 231 | statusCode: 200, 232 | response: req => new Promise(resolve => { 233 | let str = '' 234 | req.on('data', (chunk: Buffer) => { 235 | str += chunk.toString('utf8') 236 | }) 237 | req.on('end', () => { 238 | resolve({ 239 | method: req.method, 240 | success: true, 241 | ...JSON.parse(str), 242 | }) 243 | }) 244 | }), 245 | }) 246 | const res = await axios.post(url, { 247 | name: 'Jay', 248 | age: 23, 249 | }) 250 | expect(res.data).toMatchObject({ 251 | method: 'POST', 252 | success: true, 253 | name: 'Jay', 254 | age: 23, 255 | }) 256 | closeServer() 257 | }) 258 | 259 | test('POST 发送表单数据正常', async () => { 260 | const { axios } = await withAxios() 261 | const { url, closeServer } = await withServer({ 262 | statusCode: 200, 263 | response: req => new Promise(resolve => { 264 | let str = '' 265 | req.on('data', (chunk: Buffer) => { 266 | str += chunk.toString('utf8') 267 | }) 268 | req.on('end', () => { 269 | resolve({ 270 | method: req.method, 271 | success: true, 272 | ...qs.parse(str), 273 | }) 274 | }) 275 | }), 276 | }) 277 | const res = await axios.post( 278 | url, 279 | qs.stringify({ 280 | name: 'Jay', 281 | age: 23, 282 | }), 283 | { 284 | headers: { 285 | 'content-type': 'application/x-www-form-urlencoded', 286 | }, 287 | }, 288 | ) 289 | expect(res.data).toMatchObject({ 290 | method: 'POST', 291 | success: true, 292 | name: 'Jay', 293 | age: '23', 294 | }) 295 | closeServer() 296 | }) 297 | 298 | test('POST 发送 PostData 表单数据正常', async () => { 299 | const { axios, PostData } = await withAxios() 300 | const { url, closeServer } = await withServer({ 301 | statusCode: 200, 302 | response: req => new Promise(resolve => { 303 | let str = '' 304 | req.on('data', (chunk: Buffer) => { 305 | str += chunk.toString('utf8') 306 | }) 307 | req.on('end', () => { 308 | resolve({ 309 | method: req.method, 310 | success: true, 311 | ...qs.parse(str), 312 | }) 313 | }) 314 | }), 315 | }) 316 | const res = await axios.post( 317 | url, 318 | new PostData({ 319 | name: 'Jay', 320 | age: 23, 321 | }), 322 | ) 323 | expect(res.data).toMatchObject({ 324 | method: 'POST', 325 | success: true, 326 | name: 'Jay', 327 | age: '23', 328 | }) 329 | closeServer() 330 | }) 331 | }) 332 | }) 333 | 334 | describe('其他', () => { 335 | test('PostData 文件上传正常', async () => { 336 | const { axios, PostData, FileData } = await withAxiosList[0]() 337 | const { url, closeServer } = await withServer({ 338 | statusCode: 200, 339 | response: req => new Promise(resolve => { 340 | let str = '' 341 | req.on('data', (chunk: Buffer) => { 342 | str += chunk.toString('utf8') 343 | }) 344 | req.on('end', () => { 345 | resolve({ 346 | method: req.method, 347 | success: str.includes('__x__') && str.includes('test.txt'), 348 | }) 349 | }) 350 | }), 351 | }) 352 | const file = new File(Array.from('hello'), 'test.txt') 353 | const res = await axios.post( 354 | url, 355 | new PostData({ 356 | x: '__x__', 357 | file: new FileData(file), 358 | }), 359 | ) 360 | expect(res.data).toMatchObject({ 361 | method: 'POST', 362 | success: true, 363 | }) 364 | closeServer() 365 | }) 366 | }) 367 | --------------------------------------------------------------------------------