├── 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 |
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 | }
--------------------------------------------------------------------------------