├── test ├── boot.ts └── helpers │ ├── cookie.spec.ts │ ├── error.spec.ts │ ├── data.spec.ts │ ├── headers.spec.ts │ ├── util.spec.ts │ └── url.spec.ts ├── examples ├── accept-upload-file │ └── .gitkeep ├── simple │ ├── app.ts │ └── index.html ├── more │ ├── index.html │ └── app.ts ├── base │ ├── index.html │ └── app.ts ├── extend │ ├── index.html │ └── app.ts ├── error │ ├── index.html │ └── app.ts ├── global.css ├── cancel │ ├── index.html │ └── app.ts ├── config │ ├── index.html │ └── app.ts ├── interceptor │ ├── index.html │ └── app.ts ├── upload-download │ ├── index.html │ └── app.ts ├── index.html ├── server2.js ├── webpack.config.js └── server.js ├── tslint.json ├── src ├── index.ts ├── helpers │ ├── cookie.ts │ ├── data.ts │ ├── error.ts │ ├── util.ts │ ├── headers.ts │ └── url.ts ├── cancel │ ├── Cancel.ts │ └── CancelToken.ts ├── core │ ├── transform.ts │ ├── InterceptorManager.ts │ ├── dispatchRequest.ts │ ├── mergeConfig.ts │ ├── Axios.ts │ └── xhr.ts ├── axios.ts ├── defaults.ts └── types │ └── index.ts ├── .gitignore ├── .editorconfig ├── README.md ├── tsconfig.json ├── .travis.yml ├── CONTRIBUTING.md ├── tools ├── gh-pages-publish.ts └── semantic-release-prepare.ts ├── LICENSE ├── rollup.config.ts ├── code-of-conduct.md └── package.json /test/boot.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/accept-upload-file/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import axios from './axios' 2 | 3 | // 暴露给 example 文件使用 4 | export * from './types' 5 | 6 | export default axios 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples/accept-upload-file 3 | coverage 4 | .nyc_output 5 | .DS_Store 6 | *.log 7 | .vscode 8 | .idea 9 | dist 10 | compiled 11 | .awcache 12 | .rpt2_cache 13 | docs 14 | -------------------------------------------------------------------------------- /examples/simple/app.ts: -------------------------------------------------------------------------------- 1 | import axios from '../../src/index' 2 | 3 | axios({ 4 | method: 'get', 5 | url: '/simple/get', 6 | params: { 7 | a: 1, 8 | b: 2 9 | } 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /src/helpers/cookie.ts: -------------------------------------------------------------------------------- 1 | 2 | const cookie = { 3 | read(name: string): string | null { 4 | const match = document.cookie.match(new RegExp(`(^|;\\s*)(${name})=([^;]*)`)) 5 | return match ? decodeURIComponent(match[3]) : null 6 | } 7 | } 8 | 9 | export default cookie -------------------------------------------------------------------------------- /src/cancel/Cancel.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class Cancel { 3 | message?: string 4 | 5 | constructor(message?: string) { 6 | this.message = message 7 | } 8 | } 9 | 10 | export function isCancel(value: any): boolean { 11 | return value instanceof Cancel 12 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /examples/more/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | More example 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Base example 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/extend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Entend example 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Simple example 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/core/transform.ts: -------------------------------------------------------------------------------- 1 | import { AxiosTransformer } from "../types"; 2 | 3 | export function transform(data: any, headers: any, fns?: AxiosTransformer | AxiosTransformer[]): any{ 4 | if (!fns) return data 5 | 6 | if (!Array.isArray(fns)) { 7 | fns = [fns] 8 | } 9 | 10 | fns.forEach(fn => { 11 | data = fn(data, headers) 12 | }) 13 | 14 | return data 15 | } -------------------------------------------------------------------------------- /examples/error/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Error example 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, 'Courier New', Courier, monospace; 3 | color: #2c3e50; 4 | } 5 | 6 | ul { 7 | line-height: 1.5em; 8 | padding-left: 1.5em; 9 | } 10 | 11 | a { 12 | color: #7f8c8d; 13 | text-decoration: none; 14 | } 15 | a:hover { 16 | color: #4fc08d; 17 | } -------------------------------------------------------------------------------- /examples/cancel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Cancel example 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/config/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Config example 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/helpers/cookie.spec.ts: -------------------------------------------------------------------------------- 1 | import cookie from '../../src/helpers/cookie' 2 | 3 | describe('helpers:cookie', () => { 4 | test('should read cookies', () => { 5 | document.cookie = 'foo=baz' 6 | expect(cookie.read('foo')).toBe('baz') 7 | }) 8 | 9 | test('should return null if cookie name is exist', () => { 10 | document.cookie = 'foo=baz' 11 | expect(cookie.read('bar')).toBeNull() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /examples/interceptor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Interceptor example 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目介绍 2 | 用 typescript 实现 axios 浏览器部分,完善的 demo 与单元测试 3 | 4 | # ts-axios 5 | - 采用 Promise API 6 | - 请求和响应配置化 7 | - 支持请求和响应数据自定义拦截器 8 | - 支持外部取消请求 9 | - 支持跨域请求携带 cookie 10 | - 支持客户端 XSRF 防御 11 | - 支持 upload/download 进度监控 12 | - 支持 http authorization 13 | - 自定义合法状态码 14 | - 自定义参数序列化 15 | - 支持配置 baseURL 16 | - axios.all axios.spread axios.getUri 17 | 18 | 所有 axios 官方库浏览器端功能已实现 19 | 20 | # 单元测试 21 | - helpers 模块测试用例编写完毕 22 | -------------------------------------------------------------------------------- /src/helpers/data.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from './util' 2 | 3 | export function transformRequest(data: any): any { 4 | if (isPlainObject(data)) { 5 | return JSON.stringify(data) 6 | } 7 | return data 8 | } 9 | 10 | export function transformResponse(data: any): any { 11 | if (typeof data === 'string') { 12 | try { 13 | data = JSON.parse(data) 14 | } catch (err) { 15 | // do nothing 16 | } 17 | } 18 | return data 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module":"es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "declarationDir": "dist/types", 14 | "outDir": "dist/lib", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ] 18 | }, 19 | "include": [ 20 | "src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - '10' 9 | - '11' 10 | - '8' 11 | - '6' 12 | script: 13 | - npm run test:prod && npm run build 14 | after_success: 15 | - npm run travis-deploy-once "npm run report-coverage" 16 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run deploy-docs"; fi 17 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run semantic-release"; fi 18 | branches: 19 | except: 20 | - /^v\d+\.\d+\.\d+$/ 21 | -------------------------------------------------------------------------------- /examples/upload-download/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Upload-Download example 8 | 9 | 10 |

file download

11 |
12 | 13 |
14 |

15 | file upload 16 |

17 |
18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ts-axios examples 9 | 10 | 11 | 12 | 13 |

ts-axios examples

14 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/error/app.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from '../../src/index' 2 | 3 | axios({ 4 | url: '/error/get1', 5 | method: 'get' 6 | }).then(res => { 7 | console.log(res) 8 | }).catch(err => { 9 | console.log(err) 10 | }) 11 | 12 | axios({ 13 | url: '/error/get', 14 | method: 'get' 15 | }).then(res => { 16 | console.log(res) 17 | }).catch(err => { 18 | console.log(err) 19 | }) 20 | 21 | setTimeout(() => { 22 | axios({ 23 | url: '/error/get', 24 | method: 'get' 25 | }).then(res => { 26 | console.log(res) 27 | }).catch(err => { 28 | console.log(err) 29 | }) 30 | }, 5000) 31 | 32 | 33 | axios({ 34 | url: '/error/timeout', 35 | method: 'get', 36 | timeout: 2000 37 | }).then(res => { 38 | console.log(res) 39 | }).catch((err: AxiosError) => { 40 | console.log(err.message) 41 | console.log(err.config) 42 | console.log(err.code) 43 | console.log(err.request) 44 | console.log(err.isAxiosError) 45 | }) -------------------------------------------------------------------------------- /src/core/InterceptorManager.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedFn, RejectedFn } from "../types"; 2 | 3 | 4 | interface Interceptor { 5 | resolved: ResolvedFn 6 | rejected?: RejectedFn 7 | } 8 | 9 | export default class InterceptorManager { 10 | private interceptors: Array | null> 11 | 12 | constructor() { 13 | this.interceptors = [] 14 | } 15 | 16 | use(resolved: ResolvedFn, rejected: RejectedFn): number { 17 | this.interceptors.push({ 18 | resolved, 19 | rejected 20 | }) 21 | return this.interceptors.length - 1 22 | } 23 | 24 | forEach(fn: (interceptor: Interceptor) => void): void { 25 | this.interceptors.forEach(interceptor => { 26 | if (interceptor !== null) { 27 | fn(interceptor) 28 | } 29 | }) 30 | } 31 | 32 | eject(id: number) { 33 | if (this.interceptors[id]) { 34 | this.interceptors[id] = null 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/helpers/error.spec.ts: -------------------------------------------------------------------------------- 1 | import { createError } from '../../src/helpers/error' 2 | import { AxiosRequestConfig, AxiosResponse } from '../../src/types' 3 | 4 | describe('helpers:error', () => { 5 | test('should create an Error with message, config, code, request, response and isAxiosError', () => { 6 | const request = new XMLHttpRequest() 7 | const config: AxiosRequestConfig = { method: 'post' } 8 | const response: AxiosResponse = { 9 | status: 200, 10 | statusText: 'OK', 11 | headers: null, 12 | request, 13 | config, 14 | data: { foo: 'bar' } 15 | } 16 | const error = createError('Boom!', config, 'SOMETHING', request, response) 17 | expect(error instanceof Error).toBeTruthy() 18 | expect(error.message).toBe('Boom!') 19 | expect(error.config).toBe(config) 20 | expect(error.code).toBe('SOMETHING') 21 | expect(error.request).toBe(request) 22 | expect(error.isAxiosError).toBeTruthy() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We're really glad you're reading this, because we need volunteer developers to help this project come to fruition. 👏 2 | 3 | ## Instructions 4 | 5 | These steps will guide you through contributing to this project: 6 | 7 | - Fork the repo 8 | - Clone it and install dependencies 9 | 10 | git clone https://github.com/YOUR-USERNAME/typescript-library-starter 11 | npm install 12 | 13 | Keep in mind that after running `npm install` the git repo is reset. So a good way to cope with this is to have a copy of the folder to push the changes, and the other to try them. 14 | 15 | Make and commit your changes. Make sure the commands npm run build and npm run test:prod are working. 16 | 17 | Finally send a [GitHub Pull Request](https://github.com/alexjoverm/typescript-library-starter/compare?expand=1) with a clear list of what you've done (read more [about pull requests](https://help.github.com/articles/about-pull-requests/)). Make sure all of your commits are atomic (one feature per commit). 18 | -------------------------------------------------------------------------------- /examples/server2.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const cookieParser = require('cookie-parser') 4 | 5 | const app = express() 6 | 7 | app.use(bodyParser.json()) 8 | app.use(bodyParser.urlencoded({ extended: true })) 9 | app.use(cookieParser()) 10 | 11 | const router = express.Router() 12 | 13 | const cors = { 14 | 'Access-Control-Allow-Origin': 'http://localhost:8080', 15 | 'Access-Control-Allow-Credentials': true, 16 | 'Access-Control-Allow-Methods': 'POST, GET, PUT, DELETE, OPTIONS', 17 | 'Access-Control-Allow-Headers': 'Content-Type' 18 | } 19 | 20 | router.post('/more/server2', (req, res) => { 21 | res.set(cors) 22 | res.json(req.cookies) 23 | }) 24 | 25 | router.options('/more/server2', (req, res) => { 26 | res.set(cors) 27 | res.end() 28 | }) 29 | 30 | app.use(router) 31 | 32 | const port = 8088 33 | module.exports = app.listen(port, () => { 34 | console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`) 35 | }) 36 | -------------------------------------------------------------------------------- /examples/interceptor/app.ts: -------------------------------------------------------------------------------- 1 | import axios from '../../src' 2 | 3 | axios.interceptors.request.use(config => { 4 | config.headers.test += '1' 5 | return config 6 | }) 7 | 8 | axios.interceptors.request.use(config => { 9 | config.headers.test += '2' 10 | return config 11 | }) 12 | 13 | axios.interceptors.request.use(config => { 14 | config.headers.test += '3' 15 | return config 16 | }) 17 | 18 | axios.interceptors.response.use(res => { 19 | res.data += '1' 20 | return res 21 | }) 22 | 23 | const interceptorId = axios.interceptors.response.use(res => { 24 | res.data += '2' 25 | return res 26 | }) 27 | 28 | axios.interceptors.response.use(res => { 29 | res.data += '3' 30 | return res 31 | }) 32 | 33 | axios.interceptors.response.eject(interceptorId) 34 | 35 | axios({ 36 | url: '/interceptor/get', 37 | method: 'get', 38 | headers: { 39 | test: '' 40 | } 41 | }).then(res =>{ 42 | console.log('axios res: ', res) 43 | console.log('axios res.data: ', res.data) 44 | }) 45 | -------------------------------------------------------------------------------- /tools/gh-pages-publish.ts: -------------------------------------------------------------------------------- 1 | const { cd, exec, echo, touch } = require("shelljs") 2 | const { readFileSync } = require("fs") 3 | const url = require("url") 4 | 5 | let repoUrl 6 | let pkg = JSON.parse(readFileSync("package.json") as any) 7 | if (typeof pkg.repository === "object") { 8 | if (!pkg.repository.hasOwnProperty("url")) { 9 | throw new Error("URL does not exist in repository section") 10 | } 11 | repoUrl = pkg.repository.url 12 | } else { 13 | repoUrl = pkg.repository 14 | } 15 | 16 | let parsedUrl = url.parse(repoUrl) 17 | let repository = (parsedUrl.host || "") + (parsedUrl.path || "") 18 | let ghToken = process.env.GH_TOKEN 19 | 20 | echo("Deploying docs!!!") 21 | cd("docs") 22 | touch(".nojekyll") 23 | exec("git init") 24 | exec("git add .") 25 | exec('git config user.name "yishibakaien"') 26 | exec('git config user.email "646932161@qq.com"') 27 | exec('git commit -m "docs(docs): update gh-pages"') 28 | exec( 29 | `git push --force --quiet "https://${ghToken}@${repository}" master:gh-pages` 30 | ) 31 | echo("Docs deployed!!") 32 | -------------------------------------------------------------------------------- /examples/cancel/app.ts: -------------------------------------------------------------------------------- 1 | import axios, { Canceler } from '../../src' 2 | import { setTimeout } from 'timers'; 3 | 4 | const CancelToken = axios.CancelToken 5 | const source = CancelToken.source() 6 | 7 | axios.get('/cancel/get', { 8 | cancelToken: source.token 9 | }).catch(e => { 10 | if (axios.isCancel(e)) { 11 | console.log('Request canceled', e.message) 12 | } 13 | }) 14 | 15 | setTimeout(() => { 16 | source.cancel('Operation canceled by the user.') 17 | 18 | setTimeout(() => { 19 | axios.post('/cancel/post', { 20 | a: 1 21 | }, { 22 | cancelToken: source.token 23 | }).catch(e => { 24 | if (axios.isCancel(e)) { 25 | console.log(e.message) 26 | } 27 | }) 28 | }, 100) 29 | 30 | }, 100) 31 | 32 | let cancel: Canceler 33 | 34 | axios.get('/cancel/get', { 35 | cancelToken: new CancelToken(c => { 36 | cancel = c 37 | }) 38 | }).catch(e => { 39 | if (axios.isCancel(e)) { 40 | console.log('Request canceled') 41 | } 42 | }) 43 | 44 | setTimeout(() => { 45 | cancel() 46 | }, 500); -------------------------------------------------------------------------------- /src/helpers/error.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosResponse } from "../types"; 2 | 3 | export class AxiosError extends Error { 4 | isAxiosError: boolean 5 | config: AxiosRequestConfig 6 | code?: string | null | number 7 | request?: any 8 | response?: AxiosResponse 9 | 10 | constructor( 11 | message: string, 12 | config: AxiosRequestConfig, 13 | code: string | null | number, 14 | request?: any, 15 | response?: AxiosResponse 16 | ) { 17 | super(message) 18 | this.config = config 19 | this.code = code 20 | this.request = request 21 | this.response = response 22 | this.isAxiosError = true 23 | 24 | Object.setPrototypeOf(this, AxiosError.prototype) 25 | } 26 | } 27 | 28 | export function createError( 29 | message: string, 30 | config: AxiosRequestConfig, 31 | code: string | null | number, 32 | request?: any, 33 | response?: AxiosResponse 34 | ) { 35 | const error = new AxiosError(message, config, code, request, response) 36 | return error 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 yishibakaien <646932161@qq.com> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /test/helpers/data.spec.ts: -------------------------------------------------------------------------------- 1 | import { transformRequest, transformResponse } from '../../src/helpers/data' 2 | 3 | describe('helpers: data', () => { 4 | describe('trasnformRequest', () => { 5 | test('should transform request data to string if data is a plain object', () => { 6 | const a = { a: 1 } 7 | expect(transformRequest(a)).toBe('{"a":1}') 8 | }) 9 | test('should do nothing if data is not a plain object', () => { 10 | const a = new URLSearchParams('a=b') 11 | expect(transformRequest(a)).toBe(a) 12 | }) 13 | }) 14 | 15 | describe('transformResponse', () => { 16 | test('should transform response data to object if data is a JSON string', () => { 17 | const a = '{"a":2}' 18 | expect(transformResponse(a)).toEqual({ a: 2 }) 19 | }) 20 | test('should do nothing if data is a string but not a JSON string', () => { 21 | const a = '{a: 2}' 22 | expect(transformResponse(a)).toBe('{a: 2}') 23 | }) 24 | test('should do nothing if data is not a string', () => { 25 | const a = { a: 2 } 26 | expect(transformResponse(a)).toBe(a) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/axios.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosStatic } from "./types"; 2 | import Axios from "./core/Axios"; 3 | import { extend } from "./helpers/util"; 4 | import defaults from "./defaults"; 5 | import mergeConfig from "./core/mergeConfig"; 6 | import CancelToken from "./cancel/CancelToken"; 7 | import Cancel, {isCancel} from "./cancel/Cancel"; 8 | 9 | function createInstance(config: AxiosRequestConfig): AxiosStatic { 10 | const context = new Axios(config) 11 | const instance = Axios.prototype.request.bind(context) 12 | 13 | extend(instance, context) 14 | 15 | return instance as AxiosStatic 16 | } 17 | 18 | const axios = createInstance(defaults) 19 | 20 | axios.create = function create(config) { 21 | return createInstance(mergeConfig(defaults, config)) 22 | } 23 | 24 | axios.CancelToken = CancelToken 25 | axios.Cancel = Cancel 26 | axios.isCancel = isCancel 27 | axios.all = function all(promises) { 28 | return Promise.all(promises) 29 | } 30 | axios.spread = function spread(callback) { 31 | return function wrap(arr) { 32 | return callback.apply(null, arr) 33 | } 34 | } 35 | axios.Axios = Axios 36 | 37 | export default axios 38 | -------------------------------------------------------------------------------- /src/cancel/CancelToken.ts: -------------------------------------------------------------------------------- 1 | import { CancelExecutor, CancelTokenSource, Canceler } from "../types"; 2 | import Cancel from './Cancel' 3 | 4 | 5 | interface ResolvePromise { 6 | (reason?: Cancel): void 7 | } 8 | 9 | /** 10 | * 外部实例化 CancelToken 得到 cancelToken 11 | * 此时 cancelToken.promise 处于 pending 状态 12 | * 一旦调用 cancelToken.promise.then 13 | */ 14 | export default class CancelToken { 15 | promise: Promise 16 | reason?: Cancel 17 | 18 | constructor(executor: CancelExecutor) { 19 | let resolvePromise: ResolvePromise 20 | this.promise = new Promise(resolve => { 21 | resolvePromise = resolve 22 | }) 23 | 24 | executor(message => { 25 | if (this.reason) return 26 | this.reason = new Cancel(message) 27 | resolvePromise(this.reason) 28 | }) 29 | } 30 | 31 | throwIfRequested() { 32 | if (this.reason) { 33 | throw this.reason 34 | } 35 | } 36 | 37 | static source(): CancelTokenSource { 38 | let cancel!: Canceler 39 | 40 | const token = new CancelToken(c => { 41 | cancel = c 42 | }) 43 | 44 | return { 45 | cancel, 46 | token 47 | } 48 | } 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from "./types"; 2 | import { processHeaders } from "./helpers/headers"; 3 | import { transformRequest, transformResponse } from "./helpers/data"; 4 | 5 | const defaults: AxiosRequestConfig = { 6 | method: 'get', 7 | timeout: 0, 8 | headers: { 9 | common: { 10 | Accept: 'application/json, text/plain, */*' 11 | } 12 | }, 13 | xsrfCookieName: 'XSRF-TOKEN', 14 | xsrfHeaderName: 'X-XSRF-TOKEN', 15 | 16 | transformRequest: [ 17 | function(data: any, headers: any): any { 18 | processHeaders(headers, data) 19 | return transformRequest(data) 20 | } 21 | ], 22 | transformResponse: [ 23 | function(data: any): any { 24 | return transformResponse(data) 25 | } 26 | ], 27 | validateStatus(status: number): boolean { 28 | return status >= 200 && status < 300 29 | } 30 | } 31 | 32 | const methodsNoData = ['delete', 'get', 'head', 'options'] 33 | 34 | methodsNoData.forEach(method => { 35 | defaults.headers[method] = {} 36 | }) 37 | 38 | const methodWithData = ['post', 'put', 'patch'] 39 | 40 | methodWithData.forEach(method => { 41 | defaults.headers[method] = { 42 | 'Content-Type': 'application/x-www-form-urlencoded' 43 | } 44 | }) 45 | 46 | export default defaults -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import sourceMaps from 'rollup-plugin-sourcemaps' 4 | import camelCase from 'lodash.camelcase' 5 | import typescript from 'rollup-plugin-typescript2' 6 | import json from 'rollup-plugin-json' 7 | 8 | const pkg = require('./package.json') 9 | 10 | const libraryName = 'ts-axios' 11 | 12 | export default { 13 | input: `src/${libraryName}.ts`, 14 | output: [ 15 | { file: pkg.main, name: camelCase(libraryName), format: 'umd', sourcemap: true }, 16 | { file: pkg.module, format: 'es', sourcemap: true }, 17 | ], 18 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 19 | external: [], 20 | watch: { 21 | include: 'src/**', 22 | }, 23 | plugins: [ 24 | // Allow json resolution 25 | json(), 26 | // Compile TypeScript files 27 | typescript({ useTsconfigDeclarationDir: true }), 28 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 29 | commonjs(), 30 | // Allow node_modules resolution, so you can use 'external' to control 31 | // which external modules to include in the bundle 32 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 33 | resolve(), 34 | 35 | // Resolve source maps to the original source 36 | sourceMaps(), 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /examples/extend/app.ts: -------------------------------------------------------------------------------- 1 | import axios from '../../src' 2 | 3 | axios({ 4 | url: '/extend/post', 5 | method: 'post', 6 | data: { 7 | msg: 'hello' 8 | } 9 | }) 10 | 11 | axios.request({ 12 | url: '/extend/post', 13 | method: 'post', 14 | data: { 15 | msg: 'hello axios.request' 16 | } 17 | }) 18 | 19 | axios.get('/extend/get') 20 | 21 | axios.options('/extend/options') 22 | 23 | axios.delete('/extend/delete') 24 | 25 | axios.head('/extend/head') 26 | 27 | axios.post('/extend/post', { msg: 'post' }) 28 | 29 | axios.put('/extend/put', { msg: 'put' }) 30 | 31 | axios.patch('/extend/patch', { msg: 'patch' }) 32 | 33 | 34 | // 函数重载 demo 35 | axios({ 36 | url: '/extend/post', 37 | method: 'post', 38 | data: { 39 | msg: 'hi normal' 40 | } 41 | }) 42 | 43 | axios('/extend/post', { 44 | method: 'post', 45 | data: { 46 | msg: 'hi function reload' 47 | } 48 | }) 49 | 50 | // 响应数据支持泛型 demo 51 | interface ResponseData { 52 | code: number 53 | result: T 54 | message: string 55 | } 56 | 57 | interface User { 58 | name: string 59 | age: number 60 | } 61 | 62 | function getUser() { 63 | return axios>('/extend/user') 64 | .then(res => res.data) 65 | .catch(err => console.error(err)) 66 | } 67 | 68 | async function test() { 69 | const user = await getUser() 70 | if (user) { 71 | console.log(user.result.name) 72 | } 73 | } 74 | test() -------------------------------------------------------------------------------- /src/helpers/util.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString 2 | 3 | export function isDate(val: any): val is Date { 4 | return Object.prototype.toString.call(val) === '[object Date]' 5 | } 6 | 7 | export function isObject(val: any): val is Object { 8 | return val !== null && typeof val === 'object' 9 | } 10 | 11 | export function isPlainObject(val: any): val is Object { 12 | return toString.call(val) === '[object Object]' 13 | } 14 | 15 | export function isFormData(val: any): val is FormData { 16 | return typeof val !== undefined && val instanceof FormData 17 | } 18 | 19 | export function extend(to: T, from: U): T & U { 20 | for (const key in from) { 21 | ;(to as T & U)[key] = from[key] as any 22 | } 23 | return to as T & U 24 | } 25 | 26 | export function deepMerge(...objs: any[]): any { 27 | const result = Object.create(null) 28 | 29 | objs.forEach(obj => { 30 | if (obj) { 31 | Object.keys(obj).forEach(key => { 32 | const val = obj[key] 33 | if (isPlainObject(val)) { 34 | if (isPlainObject(result[key])) { 35 | result[key] = deepMerge(result[key], val) 36 | } else { 37 | result[key] = deepMerge(val) 38 | } 39 | } else { 40 | result[key] = val 41 | } 42 | }) 43 | } 44 | }) 45 | 46 | return result 47 | } 48 | 49 | export function isURLSearchParams(val: any): val is URLSearchParams { 50 | return typeof val !== undefined && val instanceof URLSearchParams 51 | } 52 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: fs.readdirSync(__dirname).reduce((entries, dir) => { 8 | const fullDir = path.join(__dirname, dir) 9 | const entry = path.join(fullDir, 'app.ts') 10 | if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) { 11 | entries[dir] = ['webpack-hot-middleware/client', entry] 12 | } 13 | return entries 14 | }, {}), 15 | 16 | output: { 17 | path: path.join(__dirname, '__build__'), 18 | filename: '[name].js', 19 | publicPath: '/__build__/' 20 | }, 21 | 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.ts$/, 26 | enforce: 'pre', 27 | use: [ 28 | { 29 | loader: 'tslint-loader' 30 | } 31 | ] 32 | }, 33 | { 34 | test: /.tsx?$/, 35 | use: [ 36 | { 37 | loader: 'ts-loader', 38 | options: { 39 | transpileOnly: true 40 | } 41 | } 42 | ] 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [ 47 | 'style-loader', 'css-loader' 48 | ] 49 | } 50 | ] 51 | }, 52 | resolve: { 53 | extensions: ['.ts', '.tsx', '.js'] 54 | }, 55 | plugins: [ 56 | new webpack.HotModuleReplacementPlugin(), 57 | new webpack.NodeEnvironmentPlugin() 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/core/dispatchRequest.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from '../types' 2 | import xhr from './xhr' 3 | import { buildURL, isAbsoluteURL, combineURL } from '../helpers/url' 4 | import { flattenHeaders } from '../helpers/headers' 5 | import { transform } from './transform' 6 | 7 | export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise { 8 | throwIfCancellationRequested(config) 9 | precessConfig(config) 10 | return xhr(config).then(res => { 11 | return transformResponseData(res) 12 | }) 13 | } 14 | 15 | function precessConfig(config: AxiosRequestConfig): void { 16 | config.url = transformURL(config) 17 | config.data = transform(config.data, config.headers, config.transformRequest) 18 | 19 | // 这里 config.method 类型断言,可以保证运行时有值 20 | config.headers = flattenHeaders(config.headers, config.method!) 21 | } 22 | 23 | export function transformURL(config: AxiosRequestConfig): string { 24 | const { params, paramsSerializer, baseURL } = config 25 | let { url } = config 26 | if (baseURL && !isAbsoluteURL(url!)) { 27 | url = combineURL(baseURL, url) 28 | } 29 | // 这里可以保证运行时 url 是有值的 30 | return buildURL(url!, params, paramsSerializer) 31 | } 32 | 33 | function transformResponseData(res: AxiosResponse): AxiosResponse { 34 | res.data = transform(res.data, res.headers, res.config.transformResponse) 35 | return res 36 | } 37 | 38 | // 请求判断此请求是否已经被取消了,如果被取消了,再发送此请求是没有意义的 39 | function throwIfCancellationRequested(config: AxiosRequestConfig): void{ 40 | if (config.cancelToken) { 41 | config.cancelToken.throwIfRequested() 42 | } 43 | } -------------------------------------------------------------------------------- /src/helpers/headers.ts: -------------------------------------------------------------------------------- 1 | import { Method } from '../types' 2 | import { isPlainObject, deepMerge } from './util' 3 | 4 | function normalizeHeaderName(headers: any, normalizeName: string): void { 5 | if (!headers) return 6 | Object.keys(headers).forEach(name => { 7 | if (name !== normalizeName && name.toLocaleUpperCase() === normalizeName.toLocaleUpperCase()) { 8 | headers[normalizeName] = headers[name] 9 | delete headers[name] 10 | } 11 | }) 12 | } 13 | 14 | export function processHeaders(headers: any, data: any): any { 15 | normalizeHeaderName(headers, 'Content-Type') 16 | 17 | if (isPlainObject(data)) { 18 | if (headers && !headers['Content-Type']) { 19 | headers['Content-Type'] = 'application/json;charset=utf-8' 20 | } 21 | } 22 | 23 | return headers 24 | } 25 | 26 | export function parseHeaders(headers: string): any { 27 | let parsed = Object.create(null) 28 | if (!headers) { 29 | return parsed 30 | } 31 | 32 | headers.split('\r\n').forEach(line => { 33 | // 字符串可能存在多个 ":" 的情况 34 | let [key, ...vals] = line.split(':') 35 | key = key.trim().toLocaleLowerCase() 36 | if (!key) return 37 | const val = vals.join(':').trim() 38 | parsed[key] = val 39 | }) 40 | return parsed 41 | } 42 | 43 | export function flattenHeaders(headers: any, method: Method): any { 44 | if (!headers) return headers 45 | 46 | headers = deepMerge(headers.common, headers[method], headers) 47 | 48 | const methodsToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common'] 49 | 50 | methodsToDelete.forEach(method => { 51 | delete headers[method] 52 | }) 53 | 54 | return headers 55 | } 56 | -------------------------------------------------------------------------------- /examples/config/app.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosTransformer } from '../../src' 2 | import qs from 'qs' 3 | 4 | axios.defaults.headers.common['test2'] = 123 5 | 6 | axios({ 7 | url: '/config/post', 8 | method: 'post', 9 | data: qs.stringify({ a: 1 }), 10 | headers: { 11 | test: '321' 12 | } 13 | }).then(res => { 14 | console.log(res.data) 15 | }) 16 | 17 | // 请求和响应配置化 demo 18 | axios({ 19 | transformRequest: [ 20 | (function(data) { 21 | return qs.stringify(data) 22 | }), 23 | ...(axios.defaults.transformRequest as AxiosTransformer[]) 24 | ], 25 | transformResponse: [ 26 | ...(axios.defaults.transformResponse as AxiosTransformer[]), 27 | function(data) { 28 | if (typeof data === 'object') { 29 | data.b = 'transform respone mark' 30 | } 31 | return data 32 | } 33 | ], 34 | url: '/config/post', 35 | method: 'post', 36 | data: { 37 | a: 1 38 | } 39 | }).then(res => { 40 | console.log(res.data) 41 | }) 42 | 43 | // axios.create demo 44 | const instance = axios.create({ 45 | transformRequest: [ 46 | (function(data) { 47 | return qs.stringify(data) 48 | }), 49 | ...(axios.defaults.transformRequest as AxiosTransformer[]) 50 | ], 51 | transformResponse: [ 52 | ...(axios.defaults.transformResponse as AxiosTransformer[]), 53 | function(data) { 54 | if (typeof data === 'object') { 55 | data.b = 'new instance transform respone mark' 56 | } 57 | return data 58 | } 59 | ] 60 | }) 61 | 62 | instance({ 63 | url: '/config/post', 64 | method: 'post', 65 | data: { 66 | a: 1 67 | } 68 | }).then(res => console.log(res.data)) 69 | -------------------------------------------------------------------------------- /src/core/mergeConfig.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from "../types"; 2 | import { isPlainObject, deepMerge } from "../helpers/util"; 3 | 4 | const strats = Object.create(null) 5 | 6 | function defaultStrat(val1: any, val2: any): any { 7 | return typeof val2 !== 'undefined' ? val2 : val1 8 | } 9 | 10 | function fromVal2Strat(val1: any, val2: any): any { 11 | if (typeof val2 !== 'undefined') return val2 12 | } 13 | 14 | function deepMergeStrat(val1: any, val2: any): any { 15 | if (isPlainObject(val2)) { 16 | return deepMerge(val1, val2) 17 | } else if (typeof val2 !== 'undefined') { 18 | return val2 19 | } else if (isPlainObject(val1)) { 20 | return deepMerge(val1) 21 | } else if (typeof val1 === 'undefined') { 22 | return val1 23 | } 24 | } 25 | 26 | const stratKeysFromVal2 = ['url', 'params', 'data'] 27 | 28 | stratKeysFromVal2.forEach(key => { 29 | strats[key] = fromVal2Strat 30 | }) 31 | 32 | const stratKeysFromDeepMerge = ['headers', 'auth'] 33 | stratKeysFromDeepMerge.forEach(key => { 34 | strats[key] = deepMergeStrat 35 | }) 36 | 37 | export default function mergeConfig(config1: AxiosRequestConfig, config2?: AxiosRequestConfig): AxiosRequestConfig { 38 | if (!config2) { 39 | config2 = {} 40 | } 41 | 42 | const config = Object.create(null) 43 | 44 | for (const key in config2) { 45 | mergeField(key) 46 | } 47 | 48 | for (const key in config1) { 49 | if (!config2[key]) { 50 | mergeField(key) 51 | } 52 | } 53 | 54 | function mergeField(key: string): void { 55 | const strat = strats[key] || defaultStrat 56 | config[key] = strat(config1[key], config2![key]) 57 | } 58 | 59 | return config 60 | } -------------------------------------------------------------------------------- /tools/semantic-release-prepare.ts: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const { fork } = require("child_process") 3 | const colors = require("colors") 4 | 5 | const { readFileSync, writeFileSync } = require("fs") 6 | const pkg = JSON.parse( 7 | readFileSync(path.resolve(__dirname, "..", "package.json")) 8 | ) 9 | 10 | pkg.scripts.prepush = "npm run test:prod && npm run build" 11 | pkg.scripts.commitmsg = "commitlint -E HUSKY_GIT_PARAMS" 12 | 13 | writeFileSync( 14 | path.resolve(__dirname, "..", "package.json"), 15 | JSON.stringify(pkg, null, 2) 16 | ) 17 | 18 | // Call husky to set up the hooks 19 | fork(path.resolve(__dirname, "..", "node_modules", "husky", "lib", "installer", 'bin'), ['install']) 20 | 21 | console.log() 22 | console.log(colors.green("Done!!")) 23 | console.log() 24 | 25 | if (pkg.repository.url.trim()) { 26 | console.log(colors.cyan("Now run:")) 27 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 28 | console.log(colors.cyan(" semantic-release-cli setup")) 29 | console.log() 30 | console.log( 31 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 32 | ) 33 | console.log() 34 | console.log( 35 | colors.gray( 36 | 'Note: Make sure "repository.url" in your package.json is correct before' 37 | ) 38 | ) 39 | } else { 40 | console.log( 41 | colors.red( 42 | 'First you need to set the "repository.url" property in package.json' 43 | ) 44 | ) 45 | console.log(colors.cyan("Then run:")) 46 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 47 | console.log(colors.cyan(" semantic-release-cli setup")) 48 | console.log() 49 | console.log( 50 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 51 | ) 52 | } 53 | 54 | console.log() 55 | -------------------------------------------------------------------------------- /examples/upload-download/app.ts: -------------------------------------------------------------------------------- 1 | import axios from '../../src' 2 | import 'nprogress/nprogress.css' 3 | import NProgress from 'nprogress' 4 | 5 | const instance = axios.create() 6 | 7 | function calculatePercentage(loaded: number, total: number) { 8 | return Math.floor(loaded * 10) / total 9 | } 10 | 11 | loadProgressBar() 12 | function loadProgressBar() { 13 | const setupStartProgress = () => { 14 | instance.interceptors.request.use(config => { 15 | NProgress.start() 16 | return config 17 | }) 18 | } 19 | 20 | const setupUpdateProgress = () => { 21 | const update = (e: ProgressEvent) => { 22 | console.log(e) 23 | NProgress.set(calculatePercentage(e.loaded, e.total)) 24 | } 25 | instance.defaults.onDownloadProgress = update 26 | instance.defaults.onUploadProgress = update 27 | } 28 | 29 | const setupStopProgress = () => { 30 | instance.interceptors.response.use(response => { 31 | NProgress.done() 32 | return response 33 | }, error => { 34 | NProgress.done() 35 | return Promise.reject(error) 36 | }) 37 | } 38 | 39 | setupStartProgress() 40 | setupUpdateProgress() 41 | setupStopProgress() 42 | } 43 | 44 | const downloadEl = document.getElementById('download') 45 | 46 | const downloadFileURL = 'https://img.mukewang.com/5cc01a7b0001a33718720632.gif' 47 | 48 | downloadEl.addEventListener('click', e => { 49 | instance.get(downloadFileURL) 50 | .then(res => { 51 | console.log(`download file success, data.length: ${res.data.length}, data.url: ${res.config.url}`) 52 | }) 53 | }) 54 | 55 | const uploadEl = document.getElementById('upload') 56 | 57 | uploadEl.addEventListener('click', e => { 58 | const data = new FormData() 59 | const fileEl = document.getElementById('file') as HTMLInputElement 60 | if (fileEl.files) { 61 | data.append('file', fileEl.files[0]) 62 | instance.post('/upload-download/upload', data) 63 | .then(() => { 64 | console.log('upload file success, you can see it on ./exapmles/accept-upload-file') 65 | }) 66 | } 67 | }) 68 | -------------------------------------------------------------------------------- /examples/base/app.ts: -------------------------------------------------------------------------------- 1 | import axios from '../../src/index' 2 | 3 | // get demo 4 | axios({ 5 | method: 'get', 6 | url: '/base/get', 7 | params: { 8 | foo: ['bar', 'baz'] 9 | } 10 | }) 11 | 12 | axios({ 13 | method: 'get', 14 | url: '/base/get', 15 | params: { 16 | foo: { 17 | bar: 'baz' 18 | } 19 | } 20 | }) 21 | 22 | const date = new Date() 23 | 24 | axios({ 25 | method: 'get', 26 | url: '/base/get', 27 | params: { 28 | date 29 | } 30 | }) 31 | 32 | 33 | axios({ 34 | method: 'get', 35 | url: '/base/get', 36 | params: { 37 | foo: '@:$, ' 38 | } 39 | }) 40 | 41 | axios({ 42 | method: 'get', 43 | url: '/base/get', 44 | params: { 45 | baz: 'bar', 46 | foo: null, 47 | } 48 | }) 49 | 50 | axios({ 51 | method: 'get', 52 | url: '/base/get#hash', 53 | params: { 54 | foo: 'baz' 55 | } 56 | }) 57 | 58 | axios({ 59 | method: 'get', 60 | url: '/base/get?baz=foo', 61 | params: { 62 | foo: 'bar' 63 | } 64 | }) 65 | 66 | // post demo 67 | axios({ 68 | method: 'post', 69 | url: '/base/post', 70 | data: { 71 | foo: 'bar', 72 | baz: 2 73 | } 74 | }) 75 | 76 | const arr = new Int32Array([21, 31]) 77 | axios({ 78 | method: 'post', 79 | url: '/base/buffer', 80 | data: arr 81 | }) 82 | 83 | // precess request headers post demo 84 | 85 | axios({ 86 | method: 'post', 87 | url: '/base/post', 88 | headers: { 89 | 'content-type': 'application/json', 90 | 'Accept': 'application/json, text/plain, */*' 91 | }, 92 | data: { 93 | a: 1, 94 | b: 2 95 | } 96 | }) 97 | 98 | /** 99 | * 浏览器本身支持直接传入 URLSearchParams | FormData 等类型对象 100 | * 会自动将请求添加一个合适的 Content-Type 101 | */ 102 | const paramsString = 'q=URLUtils.searchParams&topic=api' 103 | const searchParams = new URLSearchParams(paramsString) 104 | 105 | axios({ 106 | method: 'post', 107 | url: '/base/post', 108 | data: searchParams 109 | }) 110 | 111 | // process response data demo 112 | 113 | axios({ 114 | method: 'post', 115 | url: '/base/post', 116 | data: { 117 | message: 'hello I am unconfig request.responseType reponse data', 118 | baz: 2 119 | } 120 | }).then(res => { 121 | console.log(res) 122 | }) 123 | 124 | axios({ 125 | method: 'post', 126 | url: '/base/post', 127 | responseType: 'json', 128 | data: { 129 | message: 'hello I am request.responseType === \"json\" response data', 130 | baz: 2 131 | } 132 | }).then(res => { 133 | console.log(res) 134 | }) -------------------------------------------------------------------------------- /src/helpers/url.ts: -------------------------------------------------------------------------------- 1 | import { isDate, isPlainObject, isURLSearchParams } from './util' 2 | 3 | interface URLOrigin { 4 | protocol: string 5 | host: string 6 | } 7 | 8 | function encode(val: string): string { 9 | return encodeURIComponent(val) 10 | .replace(/%40/g, '@') 11 | .replace(/%3A/gi, ':') 12 | .replace(/%24/g, '$') 13 | .replace(/%2C/gi, ',') 14 | .replace(/%20/g, '+') // 约定将 空格 号转为 + 15 | .replace(/%5B/gi, '[') 16 | .replace(/%5D/gi, ']') 17 | } 18 | 19 | export function buildURL( 20 | url: string, 21 | params?: any, 22 | paramsSerializer?: (params: any) => string 23 | ): string { 24 | if (!params) { 25 | return url 26 | } 27 | 28 | let serializedParams 29 | 30 | if (paramsSerializer) { 31 | serializedParams = paramsSerializer(params) 32 | } else if (isURLSearchParams(params)) { 33 | serializedParams = params.toString() 34 | } else { 35 | const parts: string[] = [] 36 | 37 | Object.keys(params).forEach(key => { 38 | const val = params[key] 39 | if (val === null || typeof val === 'undefined') { 40 | return 41 | } 42 | let values = [] 43 | if (Array.isArray(val)) { 44 | values = val 45 | key += '[]' 46 | } else { 47 | values = [val] 48 | } 49 | values.forEach(val => { 50 | if (isDate(val)) { 51 | val = val.toISOString() 52 | } else if (isPlainObject(val)) { 53 | val = JSON.stringify(val) 54 | } 55 | parts.push(`${encode(key)}=${encode(val)}`) 56 | }) 57 | }) 58 | 59 | serializedParams = parts.join('&') 60 | } 61 | 62 | if (serializedParams) { 63 | const markIndex = url.indexOf('#') 64 | if (markIndex !== -1) { 65 | url = url.slice(0, markIndex) 66 | } 67 | url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams 68 | } 69 | 70 | return url 71 | } 72 | 73 | export function isAbsoluteURL(url: string): boolean { 74 | return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url) 75 | } 76 | 77 | export function combineURL(baseURL: string, relativeURL?: string): string { 78 | return relativeURL ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL 79 | } 80 | 81 | export function isURLSameOrigin(requestURL: string): boolean { 82 | const parsedOrigin = resolveURL(requestURL) 83 | return ( 84 | parsedOrigin.protocol === currentOrigin.protocol && parsedOrigin.host === currentOrigin.host 85 | ) 86 | } 87 | 88 | const urlParsingNode = document.createElement('a') 89 | const currentOrigin = resolveURL(window.location.href) 90 | 91 | function resolveURL(url: string): URLOrigin { 92 | // 通过创建一个 标签并设置 href 属性可以快捷的拿到 protocol 和 host 93 | urlParsingNode.setAttribute('href', url) 94 | const { protocol, host } = urlParsingNode 95 | 96 | return { 97 | protocol, 98 | host 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/more/app.ts: -------------------------------------------------------------------------------- 1 | import axios from '../../src' 2 | import qs from 'qs' 3 | 4 | document.cookie = 'a=b' 5 | 6 | axios.get('/more/get').then(res => { 7 | console.log(res) 8 | }) 9 | 10 | axios.post('http://127.0.0.1:8088/more/server2', {}, { 11 | withCredentials: true 12 | }).then(res => { 13 | console.log(res) 14 | }) 15 | 16 | // xsrf demo 17 | const instance = axios.create({ 18 | xsrfCookieName: 'XSRF-TOKEN-D', 19 | xsrfHeaderName: 'X-XSRF-TOKEN-D' 20 | }) 21 | 22 | instance.get('/more/get').then(res => { 23 | console.log('csrf demo:', res) 24 | }) 25 | 26 | // http auth demo 27 | axios.post('/more/post', { 28 | a: 1 29 | }, { 30 | auth: { 31 | username: 'chen', 32 | password: '123456' 33 | } 34 | }).then(res => { 35 | console.log('http auth success demo', res) 36 | }) 37 | 38 | axios.post('/more/post', { 39 | a: 1 40 | }, { 41 | auth: { 42 | username: 'chen111', 43 | password: '123456' 44 | } 45 | }).then(res => { 46 | console.log('http auth fail demo', res) 47 | }).catch(err => { 48 | console.log('http auth fail demo', err) 49 | }) 50 | 51 | // 自定义合法状态码 demo 52 | axios.get('/more/304').then(res => { 53 | console.log(res) 54 | }).catch(err => { 55 | console.log(err.message) 56 | }) 57 | 58 | 59 | axios.get('/more/304', { 60 | validateStatus(status) { 61 | return status >= 200 && status < 400 62 | } 63 | }).then(res => { 64 | console.log(res) 65 | }).catch(err => { 66 | console.log(err.message) 67 | }) 68 | 69 | // 自定义 params 的解析规则 demo 70 | axios.get('/more/get', { 71 | params: new URLSearchParams('a=b&c=d') 72 | }).then(res => { 73 | console.log(res) 74 | }) 75 | 76 | axios.get('/more/get', { 77 | params: { 78 | a: 1, 79 | b: 2, 80 | c: ['a', 'b', 'c'] 81 | } 82 | }).then(res => { 83 | console.log(res) 84 | }) 85 | 86 | const instance2 = axios.create({ 87 | paramsSerializer(params) { 88 | return qs.stringify(params, { 89 | arrayFormat: 'brackets' 90 | }) 91 | } 92 | }) 93 | 94 | instance2.get('/more/get', { 95 | params: { 96 | a: 1, 97 | b: 2, 98 | c: ['a', 'b', 'c'] 99 | } 100 | }).then(res => { 101 | console.log(res) 102 | }) 103 | 104 | // custom baseURL demo 105 | const instance3 = axios.create({ 106 | baseURL: 'https://img.mukewang.com/' 107 | }) 108 | 109 | instance3.get('5cc01a7b0001a33718720632.gif') 110 | instance3.get('https://img.mukewang.com/szimg/5becd5ad0001b89306000338-360-202.jpg') 111 | 112 | 113 | // axios.all axios.spread axios.getUri demo 114 | function getA() { 115 | return axios.get('/more/A') 116 | } 117 | function getB() { 118 | return axios.get('/more/B') 119 | } 120 | 121 | axios.all([getA(), getB()]) 122 | .then(axios.spread(function(resA, resB) { 123 | console.log(resA.data) 124 | console.log(resB.data) 125 | })) 126 | 127 | axios.all([getA(), getB()]) 128 | .then(([resA, resB]) => { 129 | console.log(resA.data) 130 | console.log(resB.data) 131 | }) 132 | 133 | const fakeConfig = { 134 | baseURL: 'https://www.baidu.com', 135 | url: '/user/12345', 136 | params: { 137 | idClient: 1, 138 | idTest: 2, 139 | testString: 'thisIsATest' 140 | } 141 | } 142 | 143 | console.log('axios.getUri result: ', axios.getUri(fakeConfig)) 144 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alexjovermorales@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /test/helpers/headers.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseHeaders, processHeaders, flattenHeaders } from '../../src/helpers/headers' 2 | 3 | describe('helpers:headers', () => { 4 | describe('parseHeaders', () => { 5 | test('should parse headers', () => { 6 | const parsed = parseHeaders( 7 | 'Content-Type: application/json\r\n' + 8 | 'Connection: keep-alive\r\n' + 9 | 'Transfer-Encoding: chunked\r\n' + 10 | 'Date: Tue, 29 Jan 2019 15:39:44 GMT\r\n' + 11 | 'key:\r\n' + 12 | ': empty key\r\n' 13 | ) 14 | 15 | expect(parsed['content-type']).toBe('application/json') 16 | expect(parsed['connection']).toBe('keep-alive') 17 | expect(parsed['transfer-encoding']).toBe('chunked') 18 | expect(parsed['date']).toBe('Tue, 29 Jan 2019 15:39:44 GMT') 19 | expect(parsed['key']).toBe('') 20 | const emptyKeyVal = Object.values(parsed).includes('empty key') 21 | expect(emptyKeyVal).toBeFalsy() 22 | }) 23 | 24 | test('should return empty object if headers is empty string', () => { 25 | expect(parseHeaders('')).toEqual({}) 26 | }) 27 | }) 28 | 29 | describe('processHeaders', () => { 30 | test('should normalize Content-type header name', () => { 31 | const headers: any = { 32 | 'content-Type': 'foo/bar', 33 | 'Content-length': 1024 34 | } 35 | processHeaders(headers, {}) 36 | expect(headers['Content-Type']).toBe('foo/bar') 37 | expect(headers['content-Type']).toBeUndefined() 38 | expect(headers['Content-length']).toBe(1024) 39 | }) 40 | 41 | test('should set Content-Type if not set and data is plain object', () => { 42 | const headers: any = {} 43 | processHeaders(headers, { a: 1 }) 44 | expect(headers['Content-Type']).toBe('application/json;charset=utf-8') 45 | }) 46 | 47 | test('should not set Content-Type if not set and data is no plain obejct', () => { 48 | const headers: any = {} 49 | processHeaders(headers, new URLSearchParams('a=b')) 50 | expect(headers['Content-Type']).toBeUndefined() 51 | }) 52 | 53 | test('should do nothing if headers is undefined or null', () => { 54 | expect(processHeaders(undefined, {})).toBeUndefined() 55 | expect(processHeaders(null, {})).toBeNull() 56 | }) 57 | }) 58 | 59 | describe('flattenHeaders', () => { 60 | test('should flatten the headers and include common headers', () => { 61 | const headers: any = { 62 | Accept: 'application/json', 63 | common: { 64 | 'X-COMMON-HEADER': 'commonHeaderValue' 65 | }, 66 | get: { 67 | 'X-GET-HEADER': 'getHeaderValue' 68 | }, 69 | post: { 70 | 'X-POST-HEADER': 'postHeaderValue' 71 | } 72 | } 73 | 74 | // 注意:返回结果里应该没有 'X-POST-HEADER': 'postHeaderValue' 75 | expect(flattenHeaders(headers, 'get')).toEqual({ 76 | Accept: 'application/json', 77 | 'X-COMMON-HEADER': 'commonHeaderValue', 78 | 'X-GET-HEADER': 'getHeaderValue' 79 | }) 80 | }) 81 | 82 | test('should flatten the headers without common headers', () => { 83 | const headers = { 84 | Accept: 'application/json', 85 | get: { 86 | 'X-GET-HEADER': 'getHeaderValue' 87 | } 88 | } 89 | 90 | expect(flattenHeaders(headers, 'patch')).toEqual({ 91 | Accept: 'application/json' 92 | }) 93 | }) 94 | 95 | test('should do nothing if headers is undefined or null', () => { 96 | expect(flattenHeaders(undefined, 'get')).toBeUndefined() 97 | expect(flattenHeaders(null, 'post')).toBeNull() 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /test/helpers/util.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isDate, 3 | isObject, 4 | isPlainObject, 5 | isFormData, 6 | isURLSearchParams, 7 | extend, 8 | deepMerge 9 | } from '../../src/helpers/util' 10 | 11 | describe('helper:util', () => { 12 | describe('isXXX', () => { 13 | test('should validate Date', () => { 14 | expect(isDate(new Date())).toBeTruthy() 15 | expect(isDate(Date.now())).toBeFalsy() 16 | }) 17 | 18 | test('should validate Object', () => { 19 | expect(isObject({})).toBeTruthy() 20 | expect(isObject([])).toBeTruthy() 21 | expect(isObject(null)).toBeFalsy() 22 | }) 23 | 24 | test('should validate PlainObject', () => { 25 | expect(isPlainObject({})).toBeTruthy() 26 | expect(isPlainObject(new Date())).toBeFalsy() 27 | }) 28 | 29 | test('should validate isFormData', () => { 30 | expect(isFormData(new FormData())).toBeTruthy() 31 | expect(isFormData({})).toBeFalsy() 32 | }) 33 | 34 | test('should validate URLSearchParams', () => { 35 | expect(isURLSearchParams(new URLSearchParams())).toBeTruthy() 36 | expect(isURLSearchParams('foo=1&bar=2')).toBeFalsy() 37 | }) 38 | }) 39 | 40 | describe('extend', () => { 41 | test('should be mutable', () => { 42 | const a = Object.create(null) 43 | const b = { foo: 123 } 44 | 45 | extend(a, b) 46 | expect(a.foo).toBe(123) 47 | }) 48 | 49 | test('should extend properties', () => { 50 | const a = { foo: 123, bar: 456 } 51 | const b = { bar: 789 } 52 | const c = extend(a, b) 53 | expect(c.foo).toBe(123) 54 | expect(c.bar).toBe(789) 55 | }) 56 | }) 57 | 58 | describe('deepMerge', () => { 59 | test('should be immutable', () => { 60 | const a = Object.create(null) 61 | const b: any = { foo: 123 } 62 | const c: any = { bar: 456 } 63 | 64 | deepMerge(a, b, c) 65 | 66 | expect(typeof a.foo).toBe('undefined') 67 | expect(typeof a.bar).toBe('undefined') 68 | expect(typeof b.bar).toBe('undefined') 69 | expect(typeof c.foo).toBe('undefined') 70 | }) 71 | 72 | // 不改变原值 73 | test('should deepMerge properties', () => { 74 | const a = { foo: 123 } 75 | const b = { bar: 456 } 76 | const c = { foo: 789 } 77 | 78 | const d = deepMerge(a, b, c) 79 | 80 | expect(d.foo).toBe(789) 81 | expect(d.bar).toBe(456) 82 | }) 83 | 84 | // 递归调用 deepMerge 85 | test('should deepMerge recursively', () => { 86 | const a = { foo: { bar: 123 } } 87 | const b = { foo: { baz: 456 }, bar: { qux: 789 } } 88 | 89 | const c = deepMerge(a, b) 90 | 91 | expect(c).toEqual({ 92 | foo: { 93 | bar: 123, 94 | baz: 456 95 | }, 96 | bar: { 97 | qux: 789 98 | } 99 | }) 100 | }) 101 | 102 | // c.foo 和 a.foo 不是同一个引用 103 | test('should remove all references from nested object', () => { 104 | const a = { foo: { bar: 123 } } 105 | const b = {} 106 | const c = deepMerge(a, b) 107 | 108 | expect(c).toEqual({ 109 | foo: { 110 | bar: 123 111 | } 112 | }) 113 | 114 | expect(c.foo).not.toBe(a.foo) 115 | }) 116 | 117 | test('should handle null and undefined arguments', () => { 118 | expect(deepMerge(undefined, undefined)).toEqual({}) 119 | expect(deepMerge(undefined, { foo: 123 })).toEqual({ foo: 123 }) 120 | expect(deepMerge({ foo: 123 }, undefined)).toEqual({ foo: 123 }) 121 | 122 | expect(deepMerge(null, null)).toEqual({}) 123 | expect(deepMerge(null, { foo: 123 })).toEqual({ foo: 123 }) 124 | expect(deepMerge({ foo: 123 }, null)).toEqual({ foo: 123 }) 125 | }) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /src/core/Axios.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosPromise, Method, AxiosResponse, ResolvedFn, RejectedFn } from "../types"; 2 | import dispatchRequest, { transformURL } from "./dispatchRequest"; 3 | import InterceptorManager from "./InterceptorManager"; 4 | import mergeConfig from "./mergeConfig"; 5 | 6 | interface Interceptors { 7 | request: InterceptorManager 8 | response: InterceptorManager 9 | } 10 | 11 | interface PromiseChain { 12 | resolved: ResolvedFn | ((config: AxiosRequestConfig) => AxiosPromise) 13 | rejected?: RejectedFn 14 | } 15 | 16 | export default class Axios { 17 | defaults: AxiosRequestConfig 18 | interceptors: Interceptors 19 | 20 | constructor(initConfig: AxiosRequestConfig) { 21 | this.defaults = initConfig 22 | this.interceptors = { 23 | request: new InterceptorManager(), 24 | response: new InterceptorManager() 25 | } 26 | } 27 | 28 | request(url: any, config?: any): AxiosPromise { 29 | // 实现函数重载,兼容传与不传 config 30 | if (typeof url === 'string') { 31 | if (!config) { 32 | config = {} 33 | } 34 | config.url = url 35 | } else { 36 | config = url 37 | } 38 | 39 | config = mergeConfig(this.defaults, config) 40 | 41 | const chain: PromiseChain[] = [{ 42 | resolved: dispatchRequest, 43 | rejected: undefined 44 | }] 45 | 46 | this.interceptors.request.forEach(interceptor => { 47 | // request 拦截器先添加的后执行 48 | chain.unshift(interceptor) 49 | }) 50 | 51 | this.interceptors.response.forEach(interceptor => { 52 | // response 拦截器先添加的先执行 53 | chain.push(interceptor) 54 | }) 55 | 56 | let promise = Promise.resolve(config) 57 | 58 | while (chain.length) { 59 | // chain.shift() 可能是 undefined 这里需要断言 60 | const { resolved, rejected } = chain.shift()! 61 | promise = promise.then(resolved, rejected) 62 | } 63 | 64 | // return dispatchRequest(config) 65 | return promise 66 | } 67 | 68 | get(url: string, config: AxiosRequestConfig): AxiosPromise { 69 | return this._requestMethodWhithoutData('get', url, config) 70 | } 71 | 72 | delete(url: string, config: AxiosRequestConfig): AxiosPromise { 73 | return this._requestMethodWhithoutData('delete', url, config) 74 | } 75 | 76 | head(url: string, config: AxiosRequestConfig): AxiosPromise { 77 | return this._requestMethodWhithoutData('head', url, config) 78 | } 79 | 80 | options(url: string, config: AxiosRequestConfig): AxiosPromise { 81 | return this._requestMethodWhithoutData('options', url, config) 82 | } 83 | 84 | post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { 85 | return this._requestMethodWhithData('post', url, data, config) 86 | } 87 | 88 | put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { 89 | return this._requestMethodWhithData('put', url, data, config) 90 | } 91 | 92 | patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise { 93 | return this._requestMethodWhithData('patch', url, data, config) 94 | } 95 | 96 | getUri(config?: AxiosRequestConfig): string { 97 | config = mergeConfig(this.defaults, config) 98 | return transformURL(config) 99 | } 100 | 101 | _requestMethodWhithoutData(method: Method, url:string, config?: AxiosRequestConfig) { 102 | return this.request(Object.assign(config || {}, { 103 | method, 104 | url 105 | })) 106 | } 107 | 108 | _requestMethodWhithData(method: Method, url:string, data?: any, config?: AxiosRequestConfig) { 109 | return this.request(Object.assign(config || {}, { 110 | method, 111 | url, 112 | data 113 | })) 114 | } 115 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-axios", 3 | "version": "0.0.1", 4 | "description": "", 5 | "keywords": [], 6 | "main": "dist/ts-axios.umd.js", 7 | "module": "dist/ts-axios.es5.js", 8 | "typings": "dist/types/ts-axios.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "author": "yishibakaien <646932161@qq.com>", 13 | "repository": { 14 | "type": "git", 15 | "url": "" 16 | }, 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "scripts": { 22 | "dev": "node examples/server.js", 23 | "nodemon": "nodemon examples/server.js", 24 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 25 | "prebuild": "rimraf dist", 26 | "build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src", 27 | "start": "rollup -c rollup.config.ts -w", 28 | "test": "jest --coverage", 29 | "test:watch": "jest --coverage --watch", 30 | "test:prod": "npm run lint && npm run test -- --no-cache", 31 | "deploy-docs": "ts-node tools/gh-pages-publish", 32 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 33 | "commit": "git-cz", 34 | "semantic-release": "semantic-release", 35 | "semantic-release-prepare": "ts-node tools/semantic-release-prepare", 36 | "precommit": "lint-staged", 37 | "travis-deploy-once": "travis-deploy-once" 38 | }, 39 | "lint-staged": { 40 | "{src,test}/**/*.ts": [ 41 | "prettier --write", 42 | "git add" 43 | ] 44 | }, 45 | "config": { 46 | "commitizen": { 47 | "path": "node_modules/cz-conventional-changelog" 48 | } 49 | }, 50 | "jest": { 51 | "transform": { 52 | ".(ts|tsx)": "ts-jest" 53 | }, 54 | "testEnvironment": "jsdom", 55 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 56 | "moduleFileExtensions": [ 57 | "ts", 58 | "tsx", 59 | "js" 60 | ], 61 | "coveragePathIgnorePatterns": [ 62 | "/node_modules/", 63 | "/test/" 64 | ], 65 | "coverageThreshold": { 66 | "global": { 67 | "branches": 90, 68 | "functions": 95, 69 | "lines": 95, 70 | "statements": 95 71 | } 72 | }, 73 | "collectCoverageFrom": [ 74 | "src/*.{js,ts}", 75 | "src/**/*.{js,ts}" 76 | ], 77 | "setupFilesAfterEnv": [ 78 | "/test/boot.ts" 79 | ] 80 | }, 81 | "prettier": { 82 | "semi": false, 83 | "singleQuote": true 84 | }, 85 | "commitlint": { 86 | "extends": [ 87 | "@commitlint/config-conventional" 88 | ] 89 | }, 90 | "devDependencies": { 91 | "@commitlint/cli": "^7.1.2", 92 | "@commitlint/config-conventional": "^7.1.2", 93 | "@types/jest": "^24.0.13", 94 | "@types/node": "^10.11.0", 95 | "@types/nprogress": "^0.2.0", 96 | "@types/qs": "^6.5.3", 97 | "atob": "^2.1.2", 98 | "body-parser": "^1.19.0", 99 | "colors": "^1.3.2", 100 | "commitizen": "^3.0.0", 101 | "connect-multiparty": "^2.2.0", 102 | "cookie-parser": "^1.4.4", 103 | "coveralls": "^3.0.2", 104 | "cross-env": "^5.2.0", 105 | "css-loader": "^3.0.0", 106 | "cz-conventional-changelog": "^2.1.0", 107 | "express": "^4.17.1", 108 | "husky": "^1.0.1", 109 | "jest": "^24.8.0", 110 | "jest-config": "^24.8.0", 111 | "lint-staged": "^8.0.0", 112 | "lodash.camelcase": "^4.3.0", 113 | "nprogress": "^0.2.0", 114 | "prettier": "^1.14.3", 115 | "prompt": "^1.0.0", 116 | "qs": "^6.7.0", 117 | "replace-in-file": "^3.4.2", 118 | "rimraf": "^2.6.2", 119 | "rollup": "^0.67.0", 120 | "rollup-plugin-commonjs": "^9.1.8", 121 | "rollup-plugin-json": "^3.1.0", 122 | "rollup-plugin-node-resolve": "^3.4.0", 123 | "rollup-plugin-sourcemaps": "^0.4.2", 124 | "rollup-plugin-typescript2": "^0.18.0", 125 | "semantic-release": "^15.9.16", 126 | "shelljs": "^0.8.3", 127 | "style-loader": "^0.23.1", 128 | "travis-deploy-once": "^5.0.9", 129 | "ts-jest": "^24.0.2", 130 | "ts-loader": "^6.0.2", 131 | "ts-node": "^7.0.1", 132 | "tslint": "^5.11.0", 133 | "tslint-config-prettier": "^1.15.0", 134 | "tslint-config-standard": "^8.0.1", 135 | "tslint-loader": "^3.5.4", 136 | "typedoc": "^0.12.0", 137 | "typescript": "^3.4.5", 138 | "webpack": "^4.33.0", 139 | "webpack-dev-middleware": "^3.7.0", 140 | "webpack-hot-middleware": "^2.25.0" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { transformRequest } from "../helpers/data"; 2 | 3 | export type Method = 4 | | 'get' 5 | | 'GET' 6 | | 'post' 7 | | 'POST' 8 | | 'delete' 9 | | 'DELETE' 10 | | 'head' 11 | | 'HEAD' 12 | | 'options' 13 | | 'OPTIONS' 14 | | 'put' 15 | | 'PUT' 16 | | 'patch' 17 | | 'PATCH' 18 | 19 | export interface AxiosRequestConfig { 20 | url?: string 21 | method?: Method 22 | data?: any 23 | params?: any 24 | headers?: any 25 | responseType?: XMLHttpRequestResponseType 26 | timeout?: number 27 | transformRequest?: AxiosTransformer | AxiosTransformer[] 28 | transformResponse?: AxiosTransformer | AxiosTransformer[] 29 | cancelToken?: CancelToken 30 | withCredentials?: boolean 31 | xsrfCookieName?: string 32 | xsrfHeaderName?: string 33 | onDownloadProgress?: (e: ProgressEvent) => void 34 | onUploadProgress?: (e: ProgressEvent) => void 35 | auth?: AxiosBasicCredentials 36 | validateStatus?: (status: number) => boolean 37 | paramsSerializer?: (params: any) => string 38 | baseURL?: string 39 | 40 | [propName: string]: any 41 | } 42 | 43 | export interface AxiosResponse { 44 | data: T 45 | status: number 46 | statusText: string 47 | headers: any 48 | config: AxiosRequestConfig 49 | request: any 50 | } 51 | 52 | export interface AxiosPromise extends Promise> { 53 | 54 | } 55 | 56 | export interface AxiosError extends Error { 57 | isAxiosError: boolean 58 | config: AxiosRequestConfig 59 | code?: string | null | number 60 | request?: any 61 | response?: AxiosResponse 62 | } 63 | 64 | export interface Axios { 65 | defaults: AxiosRequestConfig 66 | 67 | interceptors: { 68 | request: AxiosInterceptorManager 69 | response: AxiosInterceptorManager 70 | } 71 | 72 | request(config: AxiosRequestConfig): AxiosPromise 73 | 74 | get(url: string, config?: AxiosRequestConfig): AxiosPromise 75 | 76 | delete(url: string, config?: AxiosRequestConfig): AxiosPromise 77 | 78 | head(url: string, config?: AxiosRequestConfig): AxiosPromise 79 | 80 | options(url: string, config?: AxiosRequestConfig): AxiosPromise 81 | 82 | post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise 83 | 84 | put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise 85 | 86 | patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise 87 | 88 | getUri(config?: AxiosRequestConfig): string 89 | } 90 | 91 | export interface AxiosInstance extends Axios{ 92 | (config: AxiosRequestConfig): AxiosPromise 93 | 94 | (url: string, config?: AxiosRequestConfig): AxiosPromise 95 | } 96 | 97 | export interface AxiosClassStatic { 98 | new (config: AxiosRequestConfig): Axios 99 | } 100 | 101 | export interface AxiosStatic extends AxiosInstance { 102 | create(config?: AxiosRequestConfig): AxiosInstance 103 | 104 | CancelToken: CancelTokenStatic 105 | Cancel: CancelStatic 106 | isCancel: (value: any) => boolean 107 | 108 | all(promises: Array>): Promise 109 | spread(callback: (...args: T[]) => R): (arr: T[]) => R 110 | Axios: AxiosClassStatic 111 | } 112 | 113 | export interface AxiosInterceptorManager { 114 | // 返回值的 number 是这个 interceptor 的 ID 用于 eject 的时候删除此 interceptor 115 | use(resolved: ResolvedFn, rejected?: RejectedFn): number 116 | 117 | eject(id: number): void 118 | } 119 | 120 | export interface ResolvedFn { 121 | (val: T): T | Promise 122 | } 123 | 124 | export interface RejectedFn { 125 | (error: any): any 126 | } 127 | 128 | export interface AxiosTransformer { 129 | (data: any, headers?: any): any 130 | } 131 | 132 | export interface CancelToken { 133 | promise: Promise 134 | reason?: Cancel 135 | 136 | throwIfRequested(): void 137 | } 138 | 139 | export interface Canceler { 140 | (message?: string): void 141 | } 142 | 143 | export interface CancelExecutor { 144 | (cancel: Canceler): void 145 | } 146 | 147 | export interface CancelTokenSource { 148 | token: CancelToken 149 | cancel: Canceler 150 | } 151 | 152 | export interface CancelTokenStatic { 153 | new(executor: CancelExecutor): CancelToken 154 | source(): CancelTokenSource 155 | } 156 | 157 | export interface Cancel { 158 | message?: string 159 | } 160 | 161 | export interface CancelStatic { 162 | new(message?: string): Cancel 163 | } 164 | 165 | export interface AxiosBasicCredentials { 166 | username: string 167 | password: string 168 | } -------------------------------------------------------------------------------- /src/core/xhr.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from '../types' 2 | import { parseHeaders } from '../helpers/headers' 3 | import { createError } from '../helpers/error' 4 | import { isURLSameOrigin } from '../helpers/url' 5 | import cookie from '../helpers/cookie' 6 | import { isFormData } from '../helpers/util' 7 | 8 | export default function xhr(config: AxiosRequestConfig): AxiosPromise { 9 | return new Promise((resolve, reject) => { 10 | const { 11 | data = null, 12 | url, 13 | method = 'get', 14 | headers, 15 | responseType, 16 | timeout, 17 | cancelToken, 18 | withCredentials, 19 | xsrfCookieName, 20 | xsrfHeaderName, 21 | onDownloadProgress, 22 | onUploadProgress, 23 | auth, 24 | validateStatus 25 | } = config 26 | 27 | const request = new XMLHttpRequest() 28 | 29 | // 第三个参数为 async 是否是异步请求 30 | // 这里可以保证运行时 url 是有值的 31 | request.open(method.toUpperCase(), url!, true) 32 | 33 | configureRequest() 34 | addEvents() 35 | processHeaders() 36 | processCancel() 37 | 38 | request.send(data) 39 | 40 | function configureRequest(): void { 41 | if (responseType) { 42 | request.responseType = responseType 43 | } 44 | 45 | if (timeout) { 46 | request.timeout = timeout 47 | } 48 | 49 | if (withCredentials) { 50 | request.withCredentials = withCredentials 51 | } 52 | } 53 | 54 | function addEvents(): void { 55 | request.onreadystatechange = () => { 56 | if (request.readyState !== 4) { 57 | return 58 | } 59 | if (request.status === 0) { 60 | return 61 | } 62 | const responseHeaders = parseHeaders(request.getAllResponseHeaders()) 63 | 64 | // 根据传入的 responseType 来决定返回的数据 65 | const responseData = responseType === 'text' ? request.responseText : request.response 66 | 67 | const response: AxiosResponse = { 68 | data: responseData, 69 | status: request.status, 70 | statusText: request.statusText, 71 | headers: responseHeaders, 72 | config, 73 | request 74 | } 75 | 76 | handleResponse(response) 77 | } 78 | 79 | request.onerror = () => { 80 | reject(createError(`NetWork Error`, config, null, request)) 81 | } 82 | 83 | request.ontimeout = () => { 84 | reject(createError(`Timeout of ${timeout} ms exceeded`, config, 'ECONNABORTED', request)) 85 | } 86 | 87 | if (onDownloadProgress) { 88 | request.onprogress = onDownloadProgress 89 | } 90 | 91 | if (onUploadProgress) { 92 | request.upload.onprogress = onUploadProgress 93 | } 94 | } 95 | 96 | function processHeaders(): void { 97 | /** 98 | * 如果请求是个 FormData 类型,则删除 headers['Content-Type'] 99 | * 让浏览器自动为请求带上合适的 Content-Type 100 | */ 101 | if (isFormData(data)) { 102 | delete headers['Content-Type'] 103 | } 104 | 105 | /** 106 | * 跨站请求伪造 xsrf 防御 107 | * 当请求开启了 withCredentials 或者是同源请求时 108 | * 如果存在 xsrfCookieName 则为请求 headers 带上它的值 109 | */ 110 | if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) { 111 | const xsrfValue = cookie.read(xsrfCookieName) 112 | if (xsrfValue && xsrfHeaderName) { 113 | headers[xsrfHeaderName] = xsrfValue 114 | } 115 | } 116 | 117 | if (auth) { 118 | headers['Authorization'] = `Basic ${btoa(`${auth.username} : ${auth.password}`)}` 119 | } 120 | 121 | Object.keys(headers).forEach(name => { 122 | // 如果 data 为 null headers 的 content-type 属性没有意义 123 | if (data === null && name.toLowerCase() === 'content-type') { 124 | delete headers[name] 125 | } else { 126 | request.setRequestHeader(name, headers[name]) 127 | } 128 | }) 129 | } 130 | 131 | function processCancel(): void { 132 | if (cancelToken) { 133 | cancelToken.promise.then(reason => { 134 | request.abort() 135 | reject(reason) 136 | }) 137 | } 138 | } 139 | 140 | function handleResponse(response: AxiosResponse): void { 141 | const { status } = response 142 | if (!validateStatus || validateStatus(status)) { 143 | resolve(response) 144 | } else { 145 | reject(createError( 146 | `Request failed with status code ${status}`, 147 | config, 148 | null, 149 | request, 150 | response 151 | ) 152 | ) 153 | } 154 | } 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /test/helpers/url.spec.ts: -------------------------------------------------------------------------------- 1 | import { buildURL, isAbsoluteURL, combineURL, isURLSameOrigin } from '../../src/helpers/url' 2 | 3 | describe('helpers:url', () => { 4 | describe('buildURL', () => { 5 | test('should support null params', () => { 6 | expect(buildURL('/foo')).toBe('/foo') 7 | }) 8 | 9 | test('should support params', () => { 10 | expect(buildURL('/foo', { foo: 'bar' })).toBe('/foo?foo=bar') 11 | }) 12 | 13 | test('should ignore if some param value is null', () => { 14 | expect(buildURL('/foo', { foo: 'bar', baz: null })).toBe('/foo?foo=bar') 15 | }) 16 | 17 | test('should ignore if the only param is null', () => { 18 | expect(buildURL('/foo', { foo: null })).toBe('/foo') 19 | }) 20 | 21 | test('should support object params', () => { 22 | expect(buildURL('/foo', { foo: { bar: 'baz' } })).toBe( 23 | '/foo?foo=' + encodeURI('{"bar":"baz"}') 24 | ) 25 | }) 26 | 27 | test('should support Date params', () => { 28 | const date = new Date() 29 | expect(buildURL('/foo', { date })).toBe('/foo?date=' + date.toISOString()) 30 | }) 31 | 32 | test('should support array params', () => { 33 | expect(buildURL('/foo', { foo: ['bar', 'baz'] })).toBe('/foo?foo[]=bar&foo[]=baz') 34 | }) 35 | 36 | test('should support special params', () => { 37 | expect(buildURL('/foo', { foo: '@:$, ' })).toBe('/foo?foo=@:$,+') 38 | }) 39 | 40 | test('should support existing params', () => { 41 | expect(buildURL('/foo?foo=bar', { bar: 'baz' })).toBe('/foo?foo=bar&bar=baz') 42 | }) 43 | 44 | // params 中的参数覆盖 url 中已存在的参数 ** 此条废弃 ** 45 | // test('should replace existing params', () => { 46 | // expect( 47 | // buildURL('/foo?foo=bar', { foo: 'baz' }) 48 | // ).toBe('/foo?foo=baz') 49 | // }) 50 | 51 | // 舍弃 url 哈希值 52 | test('should correct discard url hash mark', () => { 53 | expect(buildURL('/foo?foo=bar#hash', { bar: 'baz' })).toBe('/foo?foo=bar&bar=baz') 54 | }) 55 | 56 | test('should use serializer if provided', () => { 57 | const serializer = jest.fn(() => { 58 | return 'foo=bar' 59 | }) 60 | const params = { foo: 'bar' } 61 | expect(buildURL('/foo', params, serializer)).toBe('/foo?foo=bar') 62 | expect(serializer).toHaveBeenCalled() 63 | expect(serializer).toHaveBeenCalledWith(params) 64 | }) 65 | 66 | test('should support URLSearchParams', () => { 67 | expect(buildURL('/foo', new URLSearchParams('bar=baz'))).toBe('/foo?bar=baz') 68 | }) 69 | }) 70 | 71 | describe('isAbsoluteURL', () => { 72 | test('should return true if URL begins with valid scheme name', () => { 73 | expect(isAbsoluteURL('https://www.baidu.com/q')).toBeTruthy() 74 | expect(isAbsoluteURL('custom-scheme-v1.0://baidu.com')).toBeTruthy() 75 | expect(isAbsoluteURL('HTTP://baidu.com')).toBeTruthy() 76 | }) 77 | 78 | test('should return false if URL begins with invalid scheme name', () => { 79 | expect(isAbsoluteURL('123://baidu.com')).toBeFalsy() 80 | expect(isAbsoluteURL('!valid://baidu.com')).toBeFalsy() 81 | }) 82 | 83 | test('should return true if URL is protocal-relative', () => { 84 | expect(isAbsoluteURL('//exmaple.com/')).toBeTruthy() 85 | }) 86 | 87 | test('should return false is URL is relative', () => { 88 | expect(isAbsoluteURL('/foo')).toBeFalsy() 89 | expect(isAbsoluteURL('foo')).toBeFalsy() 90 | }) 91 | }) 92 | 93 | describe('combineURL', () => { 94 | test('should combine URL', () => { 95 | expect(combineURL('https://api.baidu.com', '/user')).toBe('https://api.baidu.com/user') 96 | }) 97 | 98 | test('should remove duplicate slashes', () => { 99 | expect(combineURL('https://api.baidu.com/', '/user')).toBe('https://api.baidu.com/user') 100 | }) 101 | 102 | test('should insert missing slash', () => { 103 | expect(combineURL('https://api.baidu.com', 'user')).toBe('https://api.baidu.com/user') 104 | }) 105 | 106 | test('should not insert slash when relative url missing/empty', () => { 107 | expect(combineURL('https://api.baidu.com/user', '')).toBe('https://api.baidu.com/user') 108 | }) 109 | 110 | test('should allow a single slash for relative url', () => { 111 | expect(combineURL('https://api.baidu.com/user', '/')).toBe('https://api.baidu.com/user/') 112 | }) 113 | }) 114 | 115 | describe('isURLSameOrigin', () => { 116 | // 函数体内部会自动获取 window.location.href 与传入值进行比较 117 | test('should detect same origin', () => { 118 | expect(isURLSameOrigin(window.location.href)).toBeTruthy() 119 | }) 120 | 121 | test('should detect different origin', () => { 122 | expect(isURLSameOrigin('https://api.baidu.com/axios/axios')).toBeFalsy() 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const cookieParser = require('cookie-parser') 4 | const mutipart = require('connect-multiparty') 5 | const atob = require('atob') 6 | const webpack = require('webpack') 7 | const webpackDevMiddleware = require('webpack-dev-middleware') 8 | const webpackHotMiddleware = require('webpack-hot-middleware') 9 | const WebpackConfig = require('./webpack.config') 10 | const path = require('path') 11 | 12 | require('./server2') 13 | 14 | const app = express() 15 | const complier = webpack(WebpackConfig) 16 | 17 | app.use(webpackDevMiddleware(complier, { 18 | publicPath: '/__build__/', 19 | stats: { 20 | colors: true, 21 | chunks: false 22 | } 23 | })) 24 | 25 | app.use(webpackHotMiddleware(complier)) 26 | app.use(express.static(__dirname, { 27 | setHeaders(res) { 28 | res.cookie('XSRF-TOKEN-D', Math.random().toString(16).slice(2)) 29 | } 30 | })) 31 | 32 | app.use(express.static(__dirname)) 33 | 34 | app.use(bodyParser.json()) 35 | app.use(bodyParser.urlencoded({ extended: true })) 36 | app.use(cookieParser()) 37 | 38 | // 用于将文件上传到指定文件 39 | app.use(mutipart({ 40 | uploadDir: path.resolve(__dirname, 'accept-upload-file') 41 | })) 42 | 43 | const router = express.Router() 44 | 45 | registerSimpleRouter() 46 | 47 | registerBaseRouter() 48 | 49 | registerErrorRouter() 50 | 51 | registerExtendRouter() 52 | 53 | registerInterceptorRrouter() 54 | 55 | registerConfigRouter() 56 | 57 | registerCancelRouter() 58 | 59 | registerMoreRouter() 60 | 61 | registerUploadRouter() 62 | 63 | app.use(router) 64 | 65 | const port = process.env.PORT || 8080 66 | module.exports = app.listen(port, () => { 67 | console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`) 68 | }) 69 | 70 | function registerSimpleRouter() { 71 | router.get('/simple/get', function (req, res) { 72 | res.json({ 73 | msg: 'hello world' 74 | }) 75 | }) 76 | } 77 | 78 | function registerBaseRouter() { 79 | 80 | router.get('/base/get', function (req, res) { 81 | res.json(req.query) 82 | }) 83 | 84 | router.post('/base/post', function (req, res) { 85 | res.json(req.body) 86 | }) 87 | 88 | router.post('/base/buffer', function (req, res) { 89 | let msg = [] 90 | req.on('data', chunk => { 91 | if (chunk) { 92 | msg.push(chunk) 93 | } 94 | }) 95 | req.on('end', () => { 96 | let buf = Buffer.concat(msg) 97 | res.json(buf.toJSON()) 98 | }) 99 | }) 100 | } 101 | 102 | function registerErrorRouter() { 103 | router.get('/error/get', function (req, res) { 104 | if (Math.random() > 0.5) { 105 | res.json({ 106 | msg: 'hello world' 107 | }) 108 | } else { 109 | res.status(500) 110 | res.end() 111 | } 112 | }) 113 | router.get('/error/timeout', function (req, res) { 114 | setTimeout(() => { 115 | res.json({ 116 | msg: 'hello world' 117 | }) 118 | }, 3000) 119 | }) 120 | } 121 | 122 | function registerExtendRouter() { 123 | router.get('/extend/get', function (req, res) { 124 | res.json({ 125 | msg: 'hello world' 126 | }) 127 | }) 128 | 129 | router.options('/extend/options', function (req, res) { 130 | res.end() 131 | }) 132 | 133 | router.head('/extend/head', function (req, res) { 134 | res.end() 135 | }) 136 | 137 | router.delete('/extend/delete', function (req, res) { 138 | res.end() 139 | }) 140 | 141 | router.post('/extend/post', function (req, res) { 142 | res.json(req.body) 143 | }) 144 | 145 | router.put('/extend/put', function (req, res) { 146 | res.json(req.body) 147 | }) 148 | 149 | router.patch('/extend/patch', function (req, res) { 150 | res.json(req.body) 151 | }) 152 | 153 | // 响应数据支持泛型接口 154 | router.get('/extend/user', function (req, res) { 155 | res.json({ 156 | code: 0, 157 | message: 'ok', 158 | result: { 159 | name: 'Alice', 160 | age: 18 161 | } 162 | }) 163 | }) 164 | } 165 | 166 | function registerInterceptorRrouter() { 167 | router.get('/interceptor/get', function (req, res) { 168 | res.end('hello ') 169 | }) 170 | } 171 | 172 | function registerConfigRouter() { 173 | router.post('/config/post', function (req, res) { 174 | res.json(req.body) 175 | }) 176 | } 177 | 178 | function registerCancelRouter() { 179 | router.get('/cancel/get', function (req, res) { 180 | setTimeout(() => { 181 | res.json('hello') 182 | }, 1000) 183 | }) 184 | 185 | router.post('/cancel/post', function (req, res) { 186 | setTimeout(() => { 187 | res.json(req.body) 188 | }, 1000) 189 | }) 190 | } 191 | 192 | function registerMoreRouter() { 193 | router.get('/more/get', (req, res) => { 194 | res.json(req.cookies) 195 | }) 196 | 197 | router.post('/more/post', function (req, res) { 198 | const auth = req.headers.authorization 199 | const [type, credentials] = auth.split(' ') 200 | console.log('atob on server:', atob(credentials)) 201 | const [username, password] = atob(credentials).split(':').map(item => item.trim()) 202 | if (type === 'Basic' && username === 'chen' && password === '123456') { 203 | res.json(req.body) 204 | } else { 205 | res.status(401) 206 | res.end('UnAuthorization') 207 | } 208 | }) 209 | 210 | router.get('/more/304', function (req, res) { 211 | res.status(304) 212 | res.end() 213 | }) 214 | 215 | router.get('/more/A', function (req, res) { 216 | res.end('A') 217 | }) 218 | 219 | router.get('/more/B', function (req, res) { 220 | res.end('B') 221 | }) 222 | } 223 | 224 | function registerUploadRouter() { 225 | router.post('/upload-download/upload', function (req, res) { 226 | console.log(req.body, req.files) 227 | res.end('upload success!') 228 | }) 229 | } --------------------------------------------------------------------------------