├── .all-contributorsrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── bug_report_cn.md │ ├── feature_request.md │ └── feature_request_cn.md ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── docs ├── .vuepress │ ├── config.js │ └── public │ │ ├── logo.png │ │ └── standard.svg ├── README.md ├── config │ ├── README.md │ ├── common.md │ ├── default.md │ ├── runtime.md │ └── self.md └── guide │ ├── README.md │ ├── export-utils.md │ ├── form-data.md │ ├── installation.md │ ├── middleware.md │ └── mock.md ├── examples ├── apis-mp │ ├── fake-wx.js │ ├── index.d.ts │ ├── index.js │ └── mock.js └── apis-web │ ├── fake-fn.js │ ├── fake-get.js │ ├── fake-post.js │ ├── index.d.ts │ └── index.js ├── globals.d.ts ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── adapters │ ├── axios.js │ ├── index.js │ ├── jsonp.js │ └── wx.js ├── constants.js ├── exportUtils.js ├── index.d.ts ├── index.js ├── middlewareFns.js └── utils │ ├── combineUrls.js │ ├── fp.js │ ├── index.js │ ├── judge.js │ ├── logger.js │ ├── mp.js │ └── params.js ├── test ├── .eslintrc.js ├── __mocks__ │ └── wxMock.js └── __tests__ │ ├── axios.test.js │ ├── core.test.js │ ├── custom.test.js │ ├── exportUtils.test.js │ ├── fn.test.js │ ├── fp.test.js │ ├── jsonp.test.js │ ├── utils.test.js │ └── wx.test.js └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "tua-api", 3 | "projectOwner": "tuateam", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md", 8 | "docs/README.md" 9 | ], 10 | "imageSize": 100, 11 | "commit": true, 12 | "commitConvention": "angular", 13 | "contributors": [ 14 | { 15 | "login": "BuptStEve", 16 | "name": "StEve Young", 17 | "avatar_url": "https://avatars2.githubusercontent.com/u/11501493?v=4", 18 | "profile": "https://buptsteve.github.io", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "infra" 23 | ] 24 | }, 25 | { 26 | "login": "evinma", 27 | "name": "evinma", 28 | "avatar_url": "https://avatars2.githubusercontent.com/u/16096567?v=4", 29 | "profile": "https://github.com/evinma", 30 | "contributions": [ 31 | "code" 32 | ] 33 | } 34 | ], 35 | "contributorsPerLine": 7 36 | } 37 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:latest 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard', 3 | parserOptions: { 4 | ecmaVersion: 10, 5 | parser: 'babel-eslint', 6 | }, 7 | rules: { 8 | 'promise/param-names': 0, 9 | 'template-curly-spacing': 'off', 10 | 'comma-dangle': [2, 'always-multiline'], 11 | }, 12 | globals: { 13 | wx: true, 14 | FormData: true, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'bug: your bug...' 5 | labels: bug 6 | assignees: BuptStEve 7 | 8 | --- 9 | 10 | **Version** 11 | Version [e.g. 0.1.0] 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. If applicable, add screenshots to help explain your problem. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | If it's convenient: 24 | 25 | * Add an online address to reproduce: 26 | * codepen: https://codepen.io/ 27 | * jsfiddle: https://jsfiddle.net/ 28 | * codesandbox: https://codesandbox.io/ 29 | * Add a repository to reproduce:https://github.com/new 30 | 31 | **Expected behavior** 32 | A clear and concise description of what you expected to happen. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告 Bug 3 | about: 发现了一个 bug! 4 | title: 'bug: your bug...' 5 | labels: bug 6 | assignees: BuptStEve 7 | 8 | --- 9 | 10 | **版本** 11 | Version [e.g. 0.1.0] 12 | 13 | **描述一下 bug** 14 | 简洁清晰地描述一下 bug。如果方便的话,添加一些截图描述你的问题。 15 | 16 | **复现 bug** 17 | 复现的步骤: 18 | 19 | 1. 首先 '...' 20 | 2. 点击了 '...' 21 | 3. 滚动到了 '...' 22 | 4. 看到了错误 23 | 24 | 如果方便的话: 25 | 26 | * 复现 bug 的在线地址: 27 | * codepen: https://codepen.io/ 28 | * jsfiddle: https://jsfiddle.net/ 29 | * codesandbox: https://codesandbox.io/ 30 | * 复现 bug 的仓库地址:https://github.com/new 31 | 32 | **预期行为** 33 | 简洁清晰地描述一下预期行为。 34 | 35 | **附加上下文** 36 | 添加一些问题的相关上下文。 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'feat: your feature...' 5 | labels: enhancement 6 | assignees: BuptStEve 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 添加 Feature 3 | about: 新特性支持 4 | title: 'feat: your feature...' 5 | labels: enhancement 6 | assignees: BuptStEve 7 | 8 | --- 9 | 10 | **你的功能请求是否与某些问题相关?请描述** 11 | 简洁清晰地描述一下当前有什么问题。如果方便的话,添加一些截图描述你的问题。 12 | 13 | **描述您想要的解决方案** 14 | 简洁清晰地描述一下你想要的特性是怎样的。 15 | 16 | **描述你考虑过的备选方案** 17 | 简洁清晰地描述一下你考虑过的其他备选方案,可能会有什么问题。 18 | 19 | **附加上下文** 20 | 添加一些问题的相关上下文。 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.log 3 | coverage 4 | .DS_Store 5 | node_modules 6 | docs/.vuepress/dist/ 7 | 8 | yarn.lock 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 StEve Young 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

tua-api

2 | 3 |

让我们优雅地调用 api~

4 | 5 |

6 | 👉完整文档地址点这里👈 7 |

8 | 9 |

10 | 11 | Build Status 12 | 13 | 14 | Coverage Status 15 | 16 | 17 | dependencies 18 | 19 | 20 | Downloads per month 21 | Version 22 | Next Version 23 | License 24 | 25 |

26 | 27 | ## `tua-api` 是什么? 28 | `tua-api` 是一个针对发起 api 请求提供辅助功能的库。采用 ES6+ 语法,并采用 jest 进行了完整的单元测试。 29 | 30 | 目前已适配: 31 | 32 | * web 端:axios, fetch-jsonp 33 | * Node 端:axios 34 | * 小程序端:wx.request 35 | 36 | 37 | Edit tua-api github example 38 | 39 | 40 | ## 安装 41 | ### web 端 42 | #### 安装本体 43 | 44 | ```bash 45 | $ npm i -S tua-api 46 | # OR 47 | $ yarn add tua-api 48 | ``` 49 | 50 | 然后直接导入即可 51 | 52 | ```js 53 | import TuaApi from 'tua-api' 54 | ``` 55 | 56 | #### 配置武器 57 | 配置“武器”分为两种情况: 58 | 59 | * [已配置 CORS 跨域请求头](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS),或是没有跨域需求时,无需任何操作(默认采用的就是 `axios`)。 60 | 61 | * 若是用不了 CORS,那么就需要设置 `reqType: 'jsonp'` 借助 jsonp 实现跨域 62 | 63 | 但是 jsonp 只支持使用 get 的方式请求,所以如果需要发送 post 或其他方式的请求,还是需要使用 `axios`(服务端还是需要配置 CORS)。 64 | 65 | ### 小程序端 66 | #### 安装本体即可 67 | 68 | ```bash 69 | $ npm i -S tua-api 70 | # OR 71 | $ yarn add tua-api 72 | ``` 73 | 74 | ```js 75 | import TuaApi from 'tua-api' 76 | ``` 77 | 78 | > 小程序还用不了 npm?[@tua-mp/service](https://tuateam.github.io/tua-mp/tua-mp-service/) 了解一下? 79 | 80 | ## `tua-api` 能干什么? 81 | `tua-api` 能实现统一管理 api 配置(例如一般放在 `src/apis/` 下)。经过处理后,业务侧代码只需要这样写即可: 82 | 83 | ```js 84 | import { fooApi } from '@/apis/' 85 | 86 | fooApi 87 | .bar({ a: '1', b: '2' }) // 发起请求,a、b 是请求参数 88 | .then(console.log) // 收到响应 89 | .catch(console.error) // 处理错误 90 | ``` 91 | 92 | 不仅如此,还有一些其他功能: 93 | 94 | * 参数校验 95 | * 默认参数 96 | * 中间件(koa 风格) 97 | * ... 98 | 99 | ```js 100 | // 甚至可以更进一步和 tua-storage 配合使用 101 | import TuaStorage from 'tua-storage' 102 | import { getSyncFnMapByApis } from 'tua-api' 103 | 104 | // 本地写好的各种接口配置 105 | import * as apis from '@/apis' 106 | 107 | const tuaStorage = new TuaStorage({ 108 | syncFnMap: getSyncFnMapByApis(apis), 109 | }) 110 | 111 | const fetchParam = { 112 | key: fooApi.bar.key, 113 | syncParams: { a: 'a', b: 'b' }, 114 | 115 | // 过期时间,默认值为实例化时的值,以秒为单位 116 | expires: 10, 117 | 118 | // 是否直接调用同步函数更新数据,默认为 false 119 | // 适用于需要强制更新数据的场景,例如小程序中的下拉刷新 120 | isForceUpdate: true, 121 | 122 | // ... 123 | } 124 | 125 | tuaStorage 126 | .load(fetchParam) 127 | .then(console.log) 128 | .catch(console.error) 129 | ``` 130 | 131 | ## 怎么写 `api` 配置? 132 | 拿以下 api 地址举例: 133 | 134 | * `https://example-base.com/foo/bar/something/create` 135 | * `https://example-base.com/foo/bar/something/modify` 136 | * `https://example-base.com/foo/bar/something/delete` 137 | 138 | ### 地址结构划分 139 | 以上地址,一般将其分为`3`部分: 140 | 141 | * baseUrl: `'https://example-base.com/foo/bar'` 142 | * prefix: `'something'` 143 | * pathList: `[ 'create', 'modify', 'delete' ]` 144 | 145 | ### 文件结构 146 | `api/` 一般是这样的文件结构: 147 | 148 | ``` 149 | . 150 | └── apis 151 | ├── prefix-1.js 152 | ├── prefix-2.js 153 | ├── something.js // <-- 以上的 api 地址会放在这里,名字随意 154 | └── index.js 155 | ``` 156 | 157 | ### 基础配置内容 158 | ```js 159 | // src/apis/something.js 160 | 161 | export default { 162 | // 接口基础地址 163 | baseUrl: 'https://example-base.com/foo/bar', 164 | 165 | // 接口的中间路径 166 | prefix: 'something', 167 | 168 | // 接口地址数组 169 | pathList: [ 170 | { path: 'create' }, 171 | { path: 'modify' }, 172 | { path: 'delete' }, 173 | ], 174 | } 175 | ``` 176 | 177 | [更多配置请点击这里查看](https://tuateam.github.io/tua-api/config/common.html) 178 | 179 | ### 配置导出 180 | 最后来看一下 `apis/index.js` 该怎么写: 181 | 182 | ```js 183 | import TuaApi from 'tua-api' 184 | 185 | // 初始化 186 | const tuaApi = new TuaApi({ ... }) 187 | 188 | // 使用中间件 189 | tuaApi 190 | .use(async (ctx, next) => { 191 | // 请求发起前 192 | console.log('before: ', ctx) 193 | 194 | await next() 195 | 196 | // 响应返回后 197 | console.log('after: ', ctx) 198 | }) 199 | // 链式调用 200 | .use(...) 201 | 202 | export const fakeGet = tuaApi.getApi(require('./fake-get').default) 203 | export const fakePost = tuaApi.getApi(require('./fake-post').default) 204 | ``` 205 | 206 | 小程序端建议使用 [@tua-mp/cli](https://tuateam.github.io/tua-mp/tua-mp-cli/) 一键生成 api。 207 | 208 | ```bash 209 | $ tuamp add api 210 | ``` 211 | 212 | ### 配置的构成 213 | 在 `tua-api` 中配置分为四种: 214 | 215 | * [默认配置(调用 `new TuaApi({ ... })` 时传递的)](https://tuateam.github.io/tua-api/config/default.html) 216 | * [公共配置(和 `pathList` 同级的配置)](https://tuateam.github.io/tua-api/config/common.html) 217 | * [自身配置(`pathList` 数组中的对象上的配置)](https://tuateam.github.io/tua-api/config/self.html) 218 | * [运行配置(在实际调用接口时传递的配置)](https://tuateam.github.io/tua-api/config/runtime.html) 219 | 220 | 其中优先级自然是: 221 | 222 | `默认配置 < 公共配置 < 自身配置 < 运行配置` 223 | 224 |

225 | 👉更多配置点击这里👈 226 |

227 | 228 | ## Contributors ✨ 229 | 230 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 |
StEve Young
StEve Young

💻 📖 🚇
evinma
evinma

💻
241 | 242 | 243 | 244 | 245 | 246 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 247 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | '@babel/preset-env', 4 | { targets: { node: 'current' } }, 5 | ], 6 | ] 7 | const plugins = [ 8 | [ 9 | '@babel/plugin-proposal-decorators', 10 | { legacy: true }, 11 | ], 12 | '@babel/plugin-proposal-object-rest-spread', 13 | ] 14 | 15 | module.exports = { 16 | env: { 17 | dev: { presets, plugins }, 18 | test: { presets, plugins }, 19 | production: { 20 | presets: [ 21 | [ 22 | '@babel/preset-env', 23 | { modules: false }, 24 | ], 25 | ], 26 | plugins, 27 | }, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // https://www.npmjs.com/package/@commitlint/config-conventional 3 | extends: ['@commitlint/config-conventional'], 4 | rules: { 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const { name } = require('../../package.json') 2 | 3 | const description = '🏗 一款可配置的通用 api 请求函数生成工具' 4 | 5 | module.exports = { 6 | base: '/' + name + '/', 7 | locales: { 8 | '/': { title: name, description }, 9 | }, 10 | head: [ 11 | ['link', { rel: 'icon', href: '/logo.png' }], 12 | ], 13 | evergreen: true, 14 | serviceWorker: true, 15 | themeConfig: { 16 | repo: 'tuateam/tua-api', 17 | docsDir: 'docs', 18 | editLinks: true, 19 | lastUpdated: '上次更新', 20 | sidebarDepth: 2, 21 | editLinkText: '在 GitHub 上编辑此页', 22 | nav: [ 23 | { 24 | text: '🌱指南', 25 | link: '/guide/', 26 | }, 27 | { 28 | text: '⚙️配置', 29 | link: '/config/', 30 | }, 31 | { 32 | text: '🔥生态系统', 33 | items: [ 34 | { text: '📦通用本地存储', link: 'https://tuateam.github.io/tua-storage/' }, 35 | { text: '🖖小程序框架', link: 'https://tuateam.github.io/tua-mp/' }, 36 | { text: '🔐轻松解决滚动穿透', link: 'https://tuateam.github.io/tua-body-scroll-lock/' }, 37 | ], 38 | }, 39 | ], 40 | sidebar: { 41 | '/guide/': [ 42 | { 43 | title: '🌱指南', 44 | collapsable: false, 45 | children: [ 46 | 'installation', 47 | '', 48 | 'middleware', 49 | 'mock', 50 | 'export-utils', 51 | 'form-data', 52 | '../config/', 53 | ], 54 | }, 55 | ], 56 | '/config/': [ 57 | { 58 | title: '⚙️配置', 59 | collapsable: false, 60 | children: [ 61 | '', 62 | 'default', 63 | 'common', 64 | 'self', 65 | 'runtime', 66 | ], 67 | }, 68 | ], 69 | }, 70 | serviceWorker: { 71 | updatePopup: { 72 | message: 'New content is available.', 73 | buttonText: 'Refresh', 74 | }, 75 | }, 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuax/tua-api/89aa0f8fc11a20542227874d5b2b3529dd186f9c/docs/.vuepress/public/logo.png -------------------------------------------------------------------------------- /docs/.vuepress/public/standard.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actionText: 快速上手 → 4 | actionLink: /guide/ 5 | features: 6 | - title: 支持多端 7 | details: 支持 web 端、Node 端和小程序端 8 | - title: 支持跨域 9 | details: 默认使用 axios,也支持降级为 jsonp 10 | - title: 可配置 11 | details: 可配置接口类型、请求方式、默认参数、必填参数等属性 12 | - title: 支持 mock 13 | details: 接口数据支持 mock,方便开发调试 14 | - title: 中间件 15 | details: koa 风格中间件,方便添加各种特技 16 | footer: MIT Licensed | Copyright © 2018-present StEve Young 17 | --- 18 | 19 |

让我们优雅地调用 api~

20 | 21 |

22 | 23 | Standard - JavaScript Style 24 | 25 |

26 | 27 |

28 | 29 | Build Status 30 | 31 | 32 | Coverage Status 33 | 34 | 35 | dependencies 36 | 37 | 38 | Downloads per month 39 | Version 40 | Next Version 41 | License 42 | 43 |

44 | 45 | ## Contributors ✨ 46 | 47 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
StEve Young
StEve Young

💻 📖 🚇
evinma
evinma

💻
58 | 59 | 60 | 61 | 62 | 63 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 64 | -------------------------------------------------------------------------------- /docs/config/README.md: -------------------------------------------------------------------------------- 1 | # 配置说明 2 | 在 `tua-api` 中配置分为四种: 3 | 4 | * [默认配置(调用 `new TuaApi({ ... })` 时传递的)](./default.md) 5 | * [公共配置(和 `pathList` 同级的配置)](./common.md) 6 | * [自身配置(`pathList` 数组中的对象上的配置)](./self.md) 7 | * [运行配置(在实际调用接口时传递的配置)](./runtime.md) 8 | 9 | 其中优先级自然是: 10 | 11 | `默认配置 < 公共配置 < 自身配置 < 运行配置` 12 | -------------------------------------------------------------------------------- /docs/config/common.md: -------------------------------------------------------------------------------- 1 | # 公共配置 2 | 详细地址指的是填写在 `src/apis/foobar.js` 中的一级配置。这部分的配置优先级比默认配置高,但低于各个接口的自身配置。 3 | 4 | ## type 请求类型 5 | 重命名为 `method`,`type` 属性将在 `2.0.0+` 后废弃。 6 | 7 | ## method 请求类型 8 | 所有请求类型(可忽略大小写,可选值 OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT) 9 | 10 | ```js 11 | export default { 12 | // 忽略大小写 13 | method: 'post', 14 | } 15 | ``` 16 | 17 | ## host 接口基础地址 18 | 重命名为 `baseUrl`,`host` 属性将在 `2.0.0+` 后废弃。 19 | 20 | ## baseUrl 接口基础地址 21 | ```js 22 | export default { 23 | baseUrl: 'https://example-api.com/', 24 | } 25 | ``` 26 | 27 | ## mock 模拟接口数据 28 | * 类型:`Object`、`Function` 29 | * 默认值:`{}` 30 | 31 | 模拟接口数据,可以直接填数据,或是填函数。函数将收到 `params` 参数对象,即最终发送给接口的数据对象。 32 | 33 | ```js 34 | export default { 35 | // 对象形式 36 | mock: { code: 0, data: 'some data' }, 37 | 38 | // 函数形式 39 | mock: (params) => ({ 40 | code: params.mockCode, 41 | data: params.mockData, 42 | }), 43 | } 44 | ``` 45 | 46 | 详情参阅 [mock 章节](../guide/mock.md) 47 | 48 | ## prefix 接口中间地址 49 | 建议与文件同名,方便维护。 50 | 51 | ```js 52 | export default { 53 | prefix: 'foobar', 54 | } 55 | ``` 56 | 57 | ## reqType 请求使用库类型 58 | 即用哪个库发起请求目前支持:jsonp、axios、wx,不填则使用默认配置中的 reqType。 59 | 60 | ```js 61 | export default { 62 | reqType: 'jsonp', 63 | } 64 | ``` 65 | 66 | ## customFetch 自定义请求函数 67 | 适用于内置库覆盖不到的场景下使用,这是一个函数,将会接收接口相关配置,在函数内发起请求,返回值为一个 Promise。 68 | 69 | ```js 70 | export default { 71 | customFetch: ({ url, data, method }) => { 72 | // ... 73 | }, 74 | } 75 | ``` 76 | 77 | ::: warning 78 | 从优先级上来说这个参数和 `reqType` 互相替代,例如: 79 | 1. 低优先级的 `reqType` 会被高优先级的 `customFetch` 覆盖(反之亦然) 80 | 2. 若是 `reqType` 和 `customFetch` 在同级同时配置时,控制台发出告警,并且实际以 `customFetch` 为准。 81 | ::: 82 | 83 | ## commonParams 公共参数 84 | 有时对于所有接口都需要添加一个公共参数。 85 | 86 | 例如在小程序端,可能需要添加 `from` 参数标记这个接口是由小程序请求了。可以这么写: 87 | 88 | ```js 89 | export default { 90 | commonParams: { from: 'miniprogram' }, 91 | } 92 | ``` 93 | 94 | ::: tip 95 | 后,支持函数模式 96 | 97 | ```js 98 | export default { 99 | commonParams: (params) => ({ t: Date.now(), foo: params.foo }), 100 | } 101 | ``` 102 | ::: 103 | 104 | ## axiosOptions 透传参数配置 105 | 由于 tua-api 是依赖于 `axios` 或是 `fetch-jsop` 来发送请求的。所以势必要提供参数透传的功能。 106 | 107 | ```js 108 | export default { 109 | // 透传 `fetch-jsonp` 需要配置的参数。例如需要传递超时时间时可添加: 110 | jsonpOptions: { timeout: 10 * 1000 }, 111 | 112 | // 透传 `axios` 需要配置的参数。例如需要传递超时时间时可添加: 113 | axiosOptions: { timeout: 10 * 1000 }, 114 | } 115 | ``` 116 | 117 | ## jsonpOptions 透传参数配置 118 | 同上 119 | 120 | ## middleware 中间件函数数组 121 | 中间件采用的是 koa 风格,所以对于一个 api 请求,从发起请求到收到响应你都有充分的控制权。 122 | 123 | ```js 124 | export default { 125 | middleware: [ fn1, fn2, fn3 ], 126 | } 127 | ``` 128 | 129 | 详情参阅:[中间件进阶](../guide/middleware.md) 130 | 131 | ## header 请求头 132 | > 注意:jsonp 的请求方式用不了哟 133 | 134 | 配置静态请求头,例如 `Content-Type` 默认为 `application/x-www-form-urlencoded`,可以这么配置进行修改。 135 | 136 | ```js 137 | export default { 138 | header: { 139 | 'Content-Type': 'application/json', 140 | }, 141 | } 142 | ``` 143 | 144 | ::: tip 145 | * 如果想要发送二进制数据可以参阅 [FormData](../guide/form-data.md#formdata) 章节 146 | * 如果请求头上的一些数据是异步获取到的,比如小程序端的 `cookie`。建议配置下面的 `beforeFn` 函数或是中间件,异步设置请求头。 147 | * 若当前以 `post` 的方式发送请求,且当前没有配置 `transformRequest` 时,`tua-api` 会自动调用 `JSON.stringify`,并设置 `Content-Type` 为 `'application/json'`。 148 | ::: 149 | 150 | ## beforeFn 发起请求前钩子函数 151 | 在请求发起前执行的函数,因为是通过 `beforeFn().then(...)` 调用,所以注意要返回 Promise。 152 | 153 | 例如小程序端可以通过返回 `header` 传递 `cookie`,web 端使用 axios 时也可以用来修改 `header`。 154 | 155 | > 虽然 axios 配置是 `headers` 但为了和小程序端保持一致就都用 `header` 156 | 157 | ```js 158 | export default { 159 | beforeFn: () => Promise.resolve({ 160 | header: { cookie: '1' }, 161 | }), 162 | } 163 | ``` 164 | 165 | ## afterFn 收到响应后的钩子函数 166 | 在收到响应后执行的函数,可以不用返回 `Promise` 167 | 168 | > 注意接收的参数是一个【数组】 `[res.data, ctx]` 169 | 170 | * 第一个参数是接口返回数据对象 `{ code, data, msg }` 171 | * 第二个参数是请求相关参数的对象,例如有请求的 host、type、params、fullPath、reqTime、startTime、endTime 等等 172 | 173 | 默认值如下,即返回接口数据。 174 | 175 | ```js 176 | const afterFn = ([x]) => x 177 | ``` 178 | 179 | ::: warning 180 | * 该函数若是返回了数据,则业务侧将收到这个数据。所以在这里可以添加一些通用逻辑,处理返回的数据。 181 | * 该函数若是没有返回数据,业务侧也会收到 `res.data`。 182 | ::: 183 | 184 | ## isShowLoading (小程序 only) 185 | 所有请求发起时是否自动展示 loading(默认为 true)。 186 | 187 | 一般来说都是需要展示 loading 的,但是有些接口轮询时如果一直展示 loading 会很奇怪。 188 | 189 | ## showLoadingFn (小程序 only) 190 | 小程序中展示 loading 的方法: 191 | 192 | * 默认值: `() => wx.showLoading({ title: '加载中' })` 193 | * 可选值: `() => wx.showLoading(YOUR_OPTIONS)` 194 | * 可选值: `wx.showNavigationBarLoading` 195 | * 或者调用你自己定义的展示 loading 方法... 196 | 197 | ## hideLoadingFn (小程序 only) 198 | 小程序中隐藏 loading 的方法: 199 | 200 | * 默认值: wx.hideLoading 201 | * 可选值: wx.hideNavigationBarLoading 202 | * 或者调用你自己定义的隐藏 loading 方法... 203 | 204 | ## useGlobalMiddleware 使用全局中间件 205 | 是否使用全局中间件,默认为 true。 206 | 207 | 适用于某些接口正好不需要调用在 `tua-api` 初始化时定义的全局中间件的情况。 208 | 209 | ## pathList 各个接口自身配置数组 210 | 这个数组中填写的是接口最后的地址。 211 | 212 | ```js 213 | export default { 214 | pathList: [ 215 | { 216 | path: 'create', 217 | // 覆盖公共 middleware 218 | middleware: [], 219 | // 覆盖公共 jsonpOptions 220 | jsonpOptions: {}, 221 | }, 222 | { 223 | path: 'modify', 224 | // 覆盖公共 axiosOptions 225 | axiosOptions: {}, 226 | }, 227 | ], 228 | } 229 | ``` 230 | 231 | ::: tip 232 | 在 pathList 的接口对象中填写的配置具有最高优先级!将会覆盖上一级的同名属性。 233 | ::: 234 | 235 | `pathList` 中其他配置见下一节~ 236 | -------------------------------------------------------------------------------- /docs/config/default.md: -------------------------------------------------------------------------------- 1 | # 默认配置 2 | 默认配置指的就是在 `tua-api` 初始化时传递的配置 3 | 4 | ```js 5 | import TuaApi from 'tua-api' 6 | 7 | new TuaApi({ 8 | baseUrl, // 即原 host 9 | reqType, 10 | middleware, 11 | customFetch, 12 | axiosOptions, 13 | jsonpOptions, 14 | defaultErrorData, 15 | }) 16 | ``` 17 | 18 | ## host 接口基础地址 19 | 重命名为 `baseUrl`,`host` 属性将在 `2.0.0+` 后废弃。 20 | 21 | ## baseUrl 接口基础地址 22 | 例如 `https://example.com/api/` 23 | 24 | ## reqType 请求类型 25 | 即使用哪个库发起请求目前支持:jsonp、axios、wx,不填默认使用 axios。 26 | 27 | ## middleware 中间件函数数组 28 | 【所有】请求都会调用的中间件函数数组!适合添加一些通用逻辑,例如接口上报。 29 | 30 | ## customFetch 自定义请求函数 31 | 这是一个函数,将会接收接口相关配置,在函数内发起请求,返回值为一个 Promise。 32 | 33 | ## axiosOptions 透传 axios 配置参数 34 | 【通用】的配置,会和之后的配置合并。 35 | 36 | ## jsonpOptions 透传 fetch-jsonp 配置参数 37 | 同上 38 | 39 | ## defaultErrorData 出错时的默认数据对象 40 | 默认值是 `{ code: 999, msg: '出错啦!' }`,可以根据自己的业务需要修改。 41 | -------------------------------------------------------------------------------- /docs/config/runtime.md: -------------------------------------------------------------------------------- 1 | # 运行配置 2 | 运行配置指的是在接口实际调用时通过第二个参数传递的配置。这部分的配置优先级最高。 3 | 4 | 以下接口以导出为 `exampleApi` 为例。 5 | 6 | ```js 7 | exampleApi.foo( 8 | { ... }, // 第一个参数传接口参数 9 | { ... } // 第二个参数传接口配置 10 | ) 11 | ``` 12 | 13 | ## callback 回调函数参数的名称 14 | 通过 jsonp 发起请求时,在请求的 `url` 上都会有一个参数用来标识回调函数,例如 `callback=jsonp_1581908021389_16566`。 15 | 16 | `callback` 这个参数可以用来标识等号左边的值(不填则默认为 `callback`)。 17 | 18 | ```js 19 | exampleApi.foo( 20 | { ... }, 21 | { callback: `cb` } 22 | ) 23 | ``` 24 | 25 | 最终的请求 `url` 大概是:`/foo?cb=jsonp_1581908021389_16566`。 26 | 27 | ::: tip 28 | `callback` 其实就是透传了 `fetch-jsonp` 中的 `jsonpCallback`。 29 | ::: 30 | 31 | ## callbackName 回调函数名称 32 | 通过 jsonp 发起请求时,一般默认回调函数的名称都是由一些随机值构成,例如 `callback=jsonp_1581908021389_16566` 33 | 34 | 不过为了使用缓存一般需要添加 `callbackName`,但是注意重复请求时会报错(此时不设置 `callbackName` 即可)。 35 | 36 | ```js 37 | exampleApi.foo( 38 | { ... }, 39 | { callbackName: `fooCallback` } 40 | ) 41 | ``` 42 | 43 | 最终的请求 `url` 大概是:`/foo?callback=fooCallback`。 44 | 45 | ::: tip 46 | `callbackName` 其实就是透传了 `fetch-jsonp` 中的 `jsonpCallbackFunction`。 47 | ::: 48 | 49 | ## 其他参数 50 | 公共配置一节中的所有参数(除了 `pathList` 外),以及自身配置一节中的所有参数均有效,且优先级最高。 51 | 52 | * 详情参阅[公共配置](./common.md) 53 | * 详情参阅[自身配置](./self.md) 54 | -------------------------------------------------------------------------------- /docs/config/self.md: -------------------------------------------------------------------------------- 1 | # 自身配置 2 | 自身配置指的是填写在 `pathList` 中的配置。这部分的配置优先级比公共配置高,但低于各个接口的运行配置。 3 | 4 | 以下接口以导出为 `exampleApi` 为例。 5 | 6 | ## path 接口地址 7 | ```js 8 | export default { 9 | pathList: [ 10 | { 11 | path: 'foo-bar', 12 | }, 13 | ], 14 | } 15 | ``` 16 | 17 | 即接口地址的最后部分。默认这样调用 18 | 19 | ```js 20 | exampleApi['foo-bar']({ ... }) 21 | ``` 22 | 23 | ## name 接口名称(可省略) 24 | ```js 25 | export default { 26 | pathList: [ 27 | { 28 | path: 'foo-bar', 29 | name: 'fooBar', 30 | }, 31 | ], 32 | } 33 | ``` 34 | 35 | 有时接口地址较长或不方便直接调用,可以添加 `name` 配置重命名接口,这样就可以这样调用 36 | 37 | ```js 38 | exampleApi.fooBar({ ... }) 39 | ``` 40 | 41 | ## params 接口参数 42 | 43 | ```js 44 | export default { 45 | pathList: [ 46 | { 47 | path: 'create', 48 | // 数组形式(不推荐使用) 49 | params: [ 'a', 'b' ], 50 | }, 51 | { 52 | path: 'modify', 53 | // 对象形式(推荐使用) 54 | params: { 55 | // 默认参数 56 | a: '1', 57 | // 表示该参数在调用时必须传,以下两种写法都行 58 | b: { required: true }, 59 | c: { isRequired: true }, 60 | }, 61 | }, 62 | ], 63 | } 64 | ``` 65 | 66 | ::: tip 67 | 后,支持函数模式 68 | 69 | ```js 70 | export default { 71 | pathList: [ 72 | { 73 | ... 74 | params: (params) => ({ 75 | t: Date.now(), 76 | foo: params.foo, 77 | }), 78 | }, 79 | ], 80 | } 81 | ``` 82 | ::: 83 | 84 | ## commonParams 覆盖公共参数 85 | 有时某个接口正好不需要上一级中 `commonParams` 的参数。那么可以传递 `null` 覆盖上一级中的 `commonParams`。 86 | 87 | ## 其他参数 88 | 上一节中的所有参数(除了 `pathList` 外)均有效。 89 | 90 | 详情参阅上一节 [公共配置](./common.md) 91 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | ## `tua-api` 是什么? 3 | `tua-api` 是一个针对发起 api 请求提供辅助功能的库。采用 ES6+ 语法,并采用 jest 进行了完整的单元测试。 4 | 5 | 目前已适配: 6 | 7 | * web 端:axios, fetch-jsonp 8 | * Node 端:axios 9 | * 小程序端:wx.request 10 | 11 | ## `tua-api` 能干什么? 12 | `tua-api` 能实现统一管理 api 配置(例如一般放在 `src/apis/` 下)。经过处理后,业务侧代码只需要这样写即可: 13 | 14 | ```js 15 | import { fooApi } from '@/apis/' 16 | 17 | fooApi 18 | .bar({ a: '1', b: '2' }) // 发起请求,a、b 是请求参数 19 | .then(console.log) // 收到响应 20 | .catch(console.error) // 处理错误 21 | ``` 22 | 23 | 不仅如此,还有一些其他功能: 24 | 25 | * 参数校验 26 | * 默认参数 27 | * 中间件(koa 风格) 28 | * ... 29 | 30 | ```js 31 | // 甚至可以更进一步和 tua-storage 配合使用 32 | import TuaStorage from 'tua-storage' 33 | import { getSyncFnMapByApis } from 'tua-api' 34 | 35 | // 本地写好的各种接口配置 36 | import * as apis from '@/apis' 37 | 38 | const tuaStorage = new TuaStorage({ 39 | syncFnMap: getSyncFnMapByApis(apis), 40 | }) 41 | 42 | const fetchParam = { 43 | key: fooApi.bar.key, 44 | syncParams: { a: 'a', b: 'b' }, 45 | 46 | // 过期时间,默认值为实例化时的值,以秒为单位 47 | expires: 10, 48 | 49 | // 是否直接调用同步函数更新数据,默认为 false 50 | // 适用于需要强制更新数据的场景,例如小程序中的下拉刷新 51 | isForceUpdate: true, 52 | 53 | // ... 54 | } 55 | 56 | tuaStorage 57 | .load(fetchParam) 58 | .then(console.log) 59 | .catch(console.error) 60 | ``` 61 | 62 | ## 怎么写 `api` 配置? 63 | 拿以下 api 地址举例: 64 | 65 | * `https://example-base.com/foo/bar/something/create` 66 | * `https://example-base.com/foo/bar/something/modify` 67 | * `https://example-base.com/foo/bar/something/delete` 68 | 69 | ### 地址结构划分 70 | 以上地址,一般将其分为`3`部分: 71 | 72 | * baseUrl: `'https://example-base.com/foo/bar'` 73 | * prefix: `'something'` 74 | * pathList: `[ 'create', 'modify', 'delete' ]` 75 | 76 | ### 文件结构 77 | `api/` 一般是这样的文件结构: 78 | 79 | ``` 80 | . 81 | └── apis 82 | ├── prefix-1.js 83 | ├── prefix-2.js 84 | ├── something.js // <-- 以上的 api 地址会放在这里,名字随意 85 | └── index.js 86 | ``` 87 | 88 | ### 基础配置内容 89 | ```js 90 | // src/apis/something.js 91 | 92 | export default { 93 | // 接口基础地址 94 | baseUrl: 'https://example-base.com/foo/bar', 95 | 96 | // 接口的中间路径 97 | prefix: 'something', 98 | 99 | // 接口地址数组 100 | pathList: [ 101 | { path: 'create' }, 102 | { path: 'modify' }, 103 | { path: 'delete' }, 104 | ], 105 | } 106 | ``` 107 | 108 | [更多配置请点击这里查看](../config/common.md) 109 | 110 | ### 配置导出 111 | 最后来看一下 `apis/index.js` 该怎么写: 112 | 113 | ```js 114 | import TuaApi from 'tua-api' 115 | 116 | // 初始化 117 | const tuaApi = new TuaApi({ ... }) 118 | 119 | // 使用中间件 120 | tuaApi 121 | .use(async (ctx, next) => { 122 | // 请求发起前 123 | console.log('before: ', ctx) 124 | 125 | await next() 126 | 127 | // 响应返回后 128 | console.log('after: ', ctx) 129 | }) 130 | // 链式调用 131 | .use(...) 132 | 133 | export const fakeGet = tuaApi.getApi(require('./fake-get').default) 134 | export const fakePost = tuaApi.getApi(require('./fake-post').default) 135 | ``` 136 | 137 | ::: tip 138 | 小程序端建议使用 [@tua-mp/cli](https://tuateam.github.io/tua-mp/tua-mp-cli/) 一键生成 api。 139 | 140 | ```bash 141 | $ tuamp add api 142 | ``` 143 | ::: 144 | 145 | [配置的详细说明点这里](../config/) 146 | -------------------------------------------------------------------------------- /docs/guide/export-utils.md: -------------------------------------------------------------------------------- 1 | # 辅助函数 2 | ## getSyncFnMapByApis 3 | 将所有的 api 对象拍平成一个 Map,与 `tua-storage` 配合使用可以将各个发起 `api` 的函数的 `key` 与其自身绑定。 4 | 5 | ## getPreFetchFnKeysBySyncFnMap 6 | 过滤出有默认参数的接口(接口参数非数组,且不含有 isRequired)。 7 | 8 | 适用于 node 端发起请求预取数据的场景。 9 | -------------------------------------------------------------------------------- /docs/guide/form-data.md: -------------------------------------------------------------------------------- 1 | # FormData 2 | ## 发送二进制数据 3 | 日常使用中,除了简单的字符串参数以外,有时也会遇到需要发送二进制数据的场景,例如上传文件。 4 | 5 | 在旧版的 `tua-api` 中若是遇到这种请求,也能发送但比较繁琐。 6 | 7 | ```js 8 | const formData = new FormData() 9 | 10 | imgUploadApi.userUpload(null, { 11 | reqFnParams: { reqParams: formData }, 12 | axiosOptions: { transformRequest: null }, 13 | }) 14 | ``` 15 | 16 | 如上例所示,借助第二个参数[运行时配置](../config/runtime.md)设置了:数据、 `transformRequest`。 17 | 18 | 而在新版本的 `tua-api` 中,只要这么调用即可: 19 | 20 | ```js 21 | const formData = new FormData() 22 | 23 | imgUploadApi.userUpload(formData) 24 | ``` 25 | 26 | 实现原理是 `tua-api` 在底层判断出接收的接口参数是 `FormData` 类型的数据,自动设置了 `transformRequest`。 27 | 28 | > 小程序端暂时建议使用原生的 `wx.uploadFile`。 29 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | # 安装 2 | ## web 端 3 | ### 安装本体 4 | 5 | ```bash 6 | $ npm i -S tua-api 7 | # OR 8 | $ yarn add tua-api 9 | ``` 10 | 11 | 然后直接导入即可 12 | 13 | ```js 14 | import TuaApi from 'tua-api' 15 | ``` 16 | 17 | #### 配置武器 18 | 配置“武器”分为两种情况: 19 | 20 | * [已配置 CORS 跨域请求头](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS),或是没有跨域需求时,无需任何操作(默认采用的就是 `axios`)。 21 | 22 | * 若是用不了 CORS,那么就需要设置 `reqType: 'jsonp'` 借助 jsonp 实现跨域 23 | 24 | 但是 jsonp 只支持使用 get 的方式请求,所以如果需要发送 post 或其他方式的请求,还是需要使用 `axios`(服务端还是需要配置 CORS)。 25 | 26 | ::: tip 27 | 不推荐使用 jsonp 的方式,有以下几个原因: 28 | 29 | 1.频繁报错,并且报错信息比较含糊 30 | 31 | 2.为了使用缓存一般添加 callbackName,但是重复请求会报错 32 | ::: 33 | 34 | ## 小程序端 35 | ### 安装本体即可 36 | 37 | ```bash 38 | $ npm i -S tua-api 39 | # OR 40 | $ yarn add tua-api 41 | ``` 42 | 43 | ```js 44 | import TuaApi from 'tua-api' 45 | ``` 46 | 47 | ::: tip 48 | 小程序还用不了 npm?[@tua-mp/service](https://tuateam.github.io/tua-mp/tua-mp-service/) 了解一下? 49 | ::: 50 | -------------------------------------------------------------------------------- /docs/guide/middleware.md: -------------------------------------------------------------------------------- 1 | # 中间件进阶 2 | 在这一节中聊聊中间件该怎么用、注意事项、参数含义等等。 3 | 4 | ```js 5 | export default { 6 | middleware: [ fn1, fn2, fn3 ], 7 | } 8 | ``` 9 | 10 | ## 中间件执行顺序 11 | koa 中间件的执行顺序和 redux 的正好相反,例如以上写法会以以下顺序执行: 12 | 13 | `请求参数 -> fn1 -> fn2 -> fn3 -> 响应数据 -> fn3 -> fn2 -> fn1` 14 | 15 | ## 中间件写法 16 | 17 | * 普通函数:注意一定要 `return next()` 否则 `Promise` 链就断了! 18 | * async 函数:注意一定要 `await next()`! 19 | 20 | ```js 21 | // 普通函数,注意一定要 return next() 22 | function (ctx, next) { 23 | ctx.req // 请求的各种配置 24 | ctx.res // 响应,但这时还未发起请求,所以是 undefined! 25 | ctx.startTime // 发起请求的时间 26 | 27 | // 传递控制权给下一个中间件 28 | return next().then(() => { 29 | // 注意这里才有响应! 30 | ctx.res // 响应对象 31 | ctx.res.data // 响应格式化后的数据 32 | ctx.res.rawData // 响应的原始数据 33 | ctx.reqTime // 请求花费的时间 34 | ctx.endTime // 收到响应的时间 35 | }) 36 | } 37 | 38 | // async/await 39 | async function (ctx, next) { 40 | ctx.req // 请求的各种配置 41 | 42 | // 传递控制权给下一个中间件 43 | await next() 44 | 45 | // 注意这里才有响应! 46 | ctx.res // 响应对象 47 | } 48 | ``` 49 | 50 | ## 中间件参数 51 | 52 | 以下是挂在 ctx 下的各种属性,业务侧的中间件可以改写其中某些属性达到在请求发起前,以及在收到响应后进行某些操作。 53 | 54 | | 已使用的属性名 | 含义和作用 | 55 | | --- | --- | 56 | | req | 请求 | 57 | | req.host | 接口基础地址 | 58 | | req.baseUrl | 接口基础地址 | 59 | | req.mock | 模拟的响应数据或是生成数据的函数 | 60 | | req.type | 接口请求类型 get/post... | 61 | | req.method | 接口请求类型 get/post... | 62 | | req.path | 接口结尾路径 | 63 | | req.prefix | 接口前缀 | 64 | | req.reqType | 使用什么工具发(axios/jsonp/wx) | 65 | | req.reqParams | 已添加默认参数的请求参数 | 66 | | req.callbackName | 使用 jsonp 时的回调函数名 | 67 | | req.axiosOptions | 透传 axios 配置参数 | 68 | | req.jsonpOptions | 透传 fetch-jsonp 配置参数| 69 | | req.reqFnParams | 发起请求时的参数对象(上面那些参数都会被放进来作为属性) | 70 | | --- | --- | 71 | | res | 响应 | 72 | | res.data | 响应格式化后的数据 | 73 | | res.rawData | 响应的原始数据 | 74 | | res.error | 错误对象(可以取 stack 和 message) | 75 | | res.* | [透传 axios 的配置](https://github.com/axios/axios#response-schema) | 76 | | --- | --- | 77 | | reqTime | 请求花费的时间 | 78 | | startTime | 请求开始时的时间戳 | 79 | | endTime | 收到响应时的时间戳 | 80 | -------------------------------------------------------------------------------- /docs/guide/mock.md: -------------------------------------------------------------------------------- 1 | # 数据 mock 2 | ## 静态配置 3 | 即将 mock 数据直接填在该接口的配置中。 4 | 5 | ### 简单对象 6 | 简单粗暴,填数据就完事儿了~ 7 | 8 | ```js 9 | { 10 | pathList: [ 11 | // 以 foo 接口为例 12 | { 13 | path: 'foo', 14 | 15 | // 对象形式 16 | mock: { code: 0, data: 'some data' }, 17 | }, 18 | ], 19 | } 20 | ``` 21 | 22 | ### mock 函数 23 | 使用函数形式,用法上会更灵活一些。 24 | 25 | ```js 26 | { 27 | pathList: [ 28 | // 以 foo 接口为例 29 | { 30 | path: 'foo', 31 | 32 | // 函数形式 33 | mock: (params) => ({ 34 | code: params.mockCode, 35 | data: params.mockData, 36 | }), 37 | }, 38 | ], 39 | } 40 | ``` 41 | 42 | ::: tip 43 | `params` 即最终传入接口的参数对象。 44 | ::: 45 | 46 | ```js 47 | import { exampleApi } from '@/apis/' 48 | 49 | // 填写 mock 数据 50 | const mockCode = 0 51 | const mockData = { foo: 'bar' } 52 | 53 | // 请求将收到 mock 数据 54 | exampleApi.foo({ mockCode, mockData }) 55 | .then(({ code, data }) => { 56 | console.log(code, data) // 0 {foo: "bar"} 57 | }) 58 | ``` 59 | 60 | ### 多接口公共 mock 61 | mock 属性不仅可以填在各个接口处,也可以将其放在上一级,mock 当前配置中的所有接口。 62 | 63 | ```js 64 | { 65 | // 公共 mock 66 | mock: ({ __mockData__ }) => __mockData__, 67 | 68 | pathList: [ 69 | // 自身的 mock 配置优先级更高 70 | { path: 'foo', mock: { code: 0 } }, 71 | 72 | // 没填自身 mock,则默认使用公共 mock 73 | { path: 'bar' }, 74 | 75 | // 禁用 mock 76 | { path: 'null', mock: null }, 77 | ], 78 | } 79 | ``` 80 | 81 | ```js 82 | import { exampleApi } from '@/apis/' 83 | 84 | const __mockData__ = { code: 123 } 85 | 86 | // 使用自己定义 mock 数据 87 | exampleApi.foo({ __mockData__ }) 88 | .then(({ code }) => { 89 | console.log(code) // 0 90 | }) 91 | 92 | // 使用公共的 mock 数据 93 | exampleApi.bar({ __mockData__ }) 94 | .then(({ code }) => { 95 | console.log(code) // 123 96 | }) 97 | ``` 98 | 99 | 更多配置优先级内容请参阅[配置说明](../config/)部分。 100 | 101 | ## 动态配置 102 | 即为每个导出的 `api` 函数添加 `mock` 属性,在业务侧用以下方式调用。 103 | 104 | ```js 105 | import { exampleApi } from '@/apis/' 106 | 107 | // 填写 mock 数据 108 | exampleApi.foo.mock = { 109 | code: 0, 110 | data: { foo: 'bar' }, 111 | } 112 | 113 | // 同样支持 mock 函数 114 | exampleApi.foo.mock = () => ({ 115 | code: 0, 116 | data: { foo: 'bar' }, 117 | }) 118 | 119 | // 请求将收到 mock 数据 120 | exampleApi.foo().then(({ code, data }) => { 121 | console.log(code, data) // 0 {foo: "bar"} 122 | }) 123 | ``` 124 | 125 | ## 同时配置 126 | ### 优先级 127 | 若是同时配置了静态和动态 mock,动态配置的 mock 数据优先级更高。 128 | 129 | ::: tip 130 | 优先级:动态 > 静态 131 | ::: 132 | 133 | * 接口配置 134 | ```js 135 | { 136 | pathList: [ 137 | { 138 | path: 'foo', 139 | mock: (params) => ({ code: params.mockCode }), 140 | }, 141 | ], 142 | } 143 | ``` 144 | 145 | * 业务侧 146 | ```js 147 | import { exampleApi } from '@/apis/' 148 | 149 | // 动态配置的数据将覆盖静态配置的数据 150 | exampleApi.foo.mock = { code: 1 } 151 | 152 | exampleApi.foo({ mockCode: 0 }) 153 | .then(({ code }) => { 154 | console.log(code) // 1 155 | }) 156 | ``` 157 | 158 | ### 关闭 mock 159 | 可以通过以下代码实现关闭 mock 功能。 160 | 161 | ```js 162 | import { exampleApi } from '@/apis/' 163 | 164 | // 关闭 mock 165 | exampleApi.foo.mock = null 166 | 167 | // 即使传递 mock 数据也不起作用 168 | exampleApi.foo({ mockCode: 404 }) 169 | .then(({ code }) => { 170 | console.log(code) // 实际接口的返回值 171 | }) 172 | ``` 173 | 174 | ::: tip 175 | 其实动态配置 `exampleApi.foo.mock` 的默认值就是静态配置的值,而在 `tua-api` 底层读取的就是 `exampleApi.foo.mock`。 176 | 177 | 所以自然动态配置的优先级更高,并且赋值为 `null` 即可关闭 mock。 178 | ::: 179 | -------------------------------------------------------------------------------- /examples/apis-mp/fake-wx.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 该参数表示请求的公用服务器地址。 3 | baseUrl: 'http://example-base.com/', 4 | 5 | // 该参数表示请求的中间路径,建议与文件同名,以便后期维护。 6 | prefix: 'fake-wx', 7 | 8 | // 所有请求类型 9 | /** @type { import('../../src/').Method } */ 10 | type: ('post'), 11 | 12 | // 所有请求都需要携带的参数,例如小程序中的所有接口都要携带以下参数 `from=miniprogram` 13 | commonParams: { from: 'miniprogram' }, 14 | 15 | // 是否使用在 index.js 中定义的全局中间件,默认为 true 16 | useGlobalMiddleware: false, 17 | 18 | // 所有请求发起时是否自动展示 loading(默认为 true) 19 | // isShowLoading: true, 20 | 21 | // 中间件函数数组 22 | // middleware: [], 23 | 24 | // 接口地址数组 25 | pathList: [ 26 | /** 27 | * fail 28 | */ 29 | { 30 | path: 'fail', 31 | /** 32 | * @returns {Promise} 33 | */ 34 | beforeFn: () => Promise.resolve({ 35 | header: { cookie: '123' }, 36 | }), 37 | useGlobalMiddleware: true, 38 | }, 39 | /** 40 | * anotherFail 41 | */ 42 | { 43 | path: 'fail', 44 | name: 'anotherFail', 45 | }, 46 | /** 47 | * array-data 48 | */ 49 | { 50 | name: 'arrayData', 51 | path: 'array-data', 52 | /** @type { import('../../src/').Method } */ 53 | type: ('get'), 54 | params: ['param1', 'param2'], 55 | }, 56 | /** 57 | * object-data 58 | */ 59 | { 60 | name: 'objectData', 61 | path: 'object-data', 62 | params: { 63 | param1: 1217, 64 | param2: 'steve', 65 | param3: { isRequired: true }, 66 | }, 67 | }, 68 | /** 69 | * no-beforeFn 70 | */ 71 | { 72 | name: 'noBeforeFn', 73 | path: 'no-beforeFn', 74 | commonParams: null, 75 | }, 76 | /** 77 | * hide-loading 78 | */ 79 | { 80 | name: 'hideLoading', 81 | path: 'hide-loading', 82 | // 这个接口不需要展示 loading 83 | isShowLoading: false, 84 | }, 85 | /** 86 | * type-get 87 | */ 88 | { 89 | name: 'typeGet', 90 | path: 'type-get', 91 | // 这个接口单独配置类型 92 | /** @type { import('../../src/').Method } */ 93 | type: ('get'), 94 | }, 95 | /** 96 | * unknown-type 97 | */ 98 | { 99 | name: 'unknownType', 100 | path: 'unknown-type', 101 | // 这个接口单独配置类型 102 | /** @type { import('../../src/').Method } */ 103 | type: ('foo'), 104 | }, 105 | /** 106 | * nav-loading 107 | */ 108 | { 109 | name: 'navLoading', 110 | path: 'nav-loading', 111 | showLoadingFn: wx.showNavigationBarLoading, 112 | hideLoadingFn: wx.hideNavigationBarLoading, 113 | }, 114 | ], 115 | } 116 | -------------------------------------------------------------------------------- /examples/apis-mp/index.d.ts: -------------------------------------------------------------------------------- 1 | // default response result 2 | interface Result { code: number, data: any, msg?: string } 3 | interface ReqFn { 4 | key: string 5 | mock: any 6 | params: object | string[] 7 | } 8 | interface RuntimeOptions { 9 | // for jsonp 10 | callbackName?: string 11 | [key: string]: any 12 | } 13 | interface ReqFnWithAnyParams extends ReqFn { 14 | (params?: any, options?: RuntimeOptions): Promise 15 | } 16 | 17 | export const mockApi: { 18 | 'foo': ReqFnWithAnyParams 19 | 'bar': ReqFnWithAnyParams 20 | 'null': ReqFnWithAnyParams 21 | } 22 | 23 | export const fakeWxApi: { 24 | 'fail': ReqFnWithAnyParams 25 | 'typeGet': ReqFnWithAnyParams 26 | 'noBeforeFn': ReqFnWithAnyParams 27 | 'navLoading': ReqFnWithAnyParams 28 | 'anotherFail': ReqFnWithAnyParams 29 | 'hideLoading': ReqFnWithAnyParams 30 | 'unknownType': ReqFnWithAnyParams 31 | 'arrayData': ReqFn & { 32 | ( 33 | params: { param1?: any, param2?: any }, 34 | options?: RuntimeOptions 35 | ): Promise 36 | } 37 | 'objectData': ReqFn & { 38 | ( 39 | params: { param1?: any, param2?: any, param3: any }, 40 | options?: RuntimeOptions 41 | ): Promise 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/apis-mp/index.js: -------------------------------------------------------------------------------- 1 | import TuaApi from '../../src' 2 | 3 | const tuaApi = new TuaApi() 4 | 5 | // 使用中间件 6 | tuaApi.use(async (ctx, next) => { 7 | // 请求发起前 8 | // console.log('before: ', ctx) 9 | 10 | await next() 11 | 12 | // 响应返回后 13 | // console.log('after: ', ctx) 14 | }) 15 | 16 | export const mockApi = tuaApi.getApi(require('./mock').default) 17 | export const fakeWxApi = tuaApi.getApi(require('./fake-wx').default) 18 | -------------------------------------------------------------------------------- /examples/apis-mp/mock.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 该参数表示请求的公用服务器地址。 3 | host: 'http://example-base.com/', 4 | 5 | // 该参数表示请求的中间路径,建议与文件同名,以便后期维护。 6 | prefix: 'mock', 7 | 8 | // 所有请求类型 9 | /** @type { import('../../src/').Method } */ 10 | type: ('get'), 11 | 12 | // 公共 mock 13 | mock: ({ __mockData__ }) => __mockData__, 14 | 15 | pathList: [ 16 | // 自身的 mock 配置优先级更高 17 | { path: 'foo', mock: { code: 500 } }, 18 | 19 | // 没填自身 mock,则默认使用公共 mock 20 | { path: 'bar' }, 21 | 22 | // 禁用 mock 23 | { path: 'null', mock: null }, 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /examples/apis-web/fake-fn.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 该参数表示请求的公用服务器地址。 3 | baseUrl: 'http://example-base.com/', 4 | 5 | // 请求的中间路径,建议与文件同名,以便后期维护。 6 | prefix: 'fake-fn', 7 | 8 | // 所有请求都需要携带的参数 9 | commonParams: (args) => ({ 10 | ...args, 11 | c: Math.random(), 12 | }), 13 | 14 | reqType: 'axios', 15 | 16 | // 接口地址数组 17 | pathList: [ 18 | /** 19 | * fn-params 20 | */ 21 | { 22 | name: 'fp', 23 | path: 'fn-params', 24 | method: 'post', 25 | params: ({ param1, param2 }) => ({ 26 | t: Math.random(), 27 | p1: param1, 28 | p2: param2, 29 | }), 30 | }, 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /examples/apis-web/fake-get.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 请求的公用服务器地址。 3 | // baseUrl: 'http://example-base.com/', 4 | 5 | // 请求的中间路径,建议与文件同名,以便后期维护。 6 | prefix: 'fake-get', 7 | 8 | // 所有请求都需要携带的参数 9 | commonParams: null, 10 | 11 | // 透传 `fetch-jsonp` 需要配置的参数。例如需要传递超时时间时可添加: 12 | jsonpOptions: { 13 | timeout: 10 * 1000, 14 | jsonpCallback: 'cb', 15 | jsonpCallbackFunction: 'cbName', 16 | }, 17 | 18 | // 透传 `axios` 需要配置的参数。例如需要传递超时时间时可添加: 19 | axiosOptions: { timeout: 10 * 1000 }, 20 | 21 | // 是否使用在 index.js 中定义的全局中间件,默认为 true 22 | useGlobalMiddleware: false, 23 | 24 | // 中间件函数数组 25 | middleware: [ 26 | // (ctx, next) => { 27 | // // 请求发起前 28 | // console.log('before: ', ctx) 29 | 30 | // return next().then(() => { 31 | // // 响应返回后 32 | // console.log('after: ', ctx) 33 | // }) 34 | // }, 35 | ], 36 | 37 | // 接口地址数组 38 | pathList: [ 39 | /** 40 | * empty-array-params 41 | */ 42 | { 43 | path: 'empty-array-params', 44 | }, 45 | /** 46 | * array-params 47 | */ 48 | { 49 | name: 'ap', 50 | path: 'array-params', 51 | params: ['param1', 'param2'], 52 | // 在这里定义将覆盖公共中间件 53 | middleware: [], 54 | }, 55 | /** 56 | * object-params 57 | */ 58 | { 59 | name: 'op', 60 | path: 'object-params', 61 | /** @type { import('../../src/').ReqType } */ 62 | reqType: (''), 63 | params: { 64 | param1: 1217, 65 | param2: 'steve', 66 | // isRequired 或者 required 都行 67 | param3: { required: true }, 68 | }, 69 | }, 70 | /** 71 | * async-common-params 72 | */ 73 | { 74 | name: 'acp', 75 | path: 'async-common-params', 76 | params: [], 77 | /** 78 | * 在这里返回的 params 会和请求的 params 合并 79 | * @returns {Promise} 80 | */ 81 | beforeFn: () => Promise.resolve({ 82 | params: { asyncCp: 'asyncCp' }, 83 | }), 84 | }, 85 | /** 86 | * req-type-axios 87 | */ 88 | { 89 | name: 'rta', 90 | path: 'req-type-axios', 91 | // 用哪个包发起请求目前支持:jsonp、axios 92 | // 如果不指定默认对于 get 请求使用 fetch-jsonp,post 请求使用 axios 93 | /** @type { import('../../src/').ReqType } */ 94 | reqType: ('axios'), 95 | /** 96 | * 在这里返回的 params 会和请求的 params 合并 97 | * @returns {Promise} 98 | */ 99 | beforeFn: () => Promise.resolve({ 100 | params: { asyncCp: 'asyncCp' }, 101 | }), 102 | }, 103 | /** 104 | * invalid-req-type 105 | */ 106 | { 107 | name: 'irt', 108 | path: 'invalid-req-type', 109 | /** @type { import('../../src/').ReqType } */ 110 | reqType: ('foobar'), 111 | }, 112 | /** 113 | * afterFn-data 114 | */ 115 | { 116 | name: 'afterData', 117 | path: 'afterFn-data', 118 | afterFn: ([data]) => ({ ...data, afterData: 'afterData' }), 119 | }, 120 | /** 121 | * no-afterFn-data 122 | */ 123 | { 124 | name: 'noAfterData', 125 | path: 'no-afterFn-data', 126 | afterFn: () => {}, 127 | }, 128 | /** 129 | * mock-object-data 130 | */ 131 | { 132 | name: 'mockObjectData', 133 | path: 'mock-object-data', 134 | mock: { code: 404, data: {} }, 135 | }, 136 | /** 137 | * mock-function-data 138 | */ 139 | { 140 | name: 'mockFnData', 141 | path: 'mock-function-data', 142 | /** @type { import('../../src/').ReqType } */ 143 | reqType: ('axios'), 144 | mock: ({ mockCode }) => ({ code: mockCode, data: {} }), 145 | }, 146 | /** 147 | * beforeFnCookie 148 | */ 149 | { 150 | name: 'beforeFnCookie', 151 | path: 'beforeFn-cookie', 152 | /** 153 | * @returns {Promise} 154 | */ 155 | beforeFn: () => Promise.resolve({ 156 | header: { cookie: '123' }, 157 | }), 158 | /** @type { import('../../src/').ReqType } */ 159 | reqType: ('axios'), 160 | }, 161 | /** 162 | * jsonp-options 163 | */ 164 | { 165 | name: 'jsonpOptions', 166 | path: 'jsonp-options', 167 | }, 168 | ], 169 | } 170 | -------------------------------------------------------------------------------- /examples/apis-web/fake-post.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // 该参数表示请求的公用服务器地址。 3 | baseUrl: 'http://example-base.com/', 4 | 5 | // 该参数表示请求的中间路径,建议与文件同名,以便后期维护。 6 | prefix: 'fake-post', 7 | 8 | /** @type { import('../../src/').Method } */ 9 | method: ('post'), 10 | 11 | // 所有请求都需要携带的参数 12 | commonParams: { common: 'params' }, 13 | 14 | // 中间件函数数组 15 | middleware: [], 16 | 17 | // 接口地址数组 18 | pathList: [ 19 | /** 20 | * empty-array-params 21 | */ 22 | { name: 'eap', path: 'empty-array-params' }, 23 | /** 24 | * array-params 25 | */ 26 | { 27 | name: 'ap', 28 | path: 'array-params', 29 | /** @type { import('../../src/').ReqType } */ 30 | reqType: ('axios'), 31 | params: ['param1', 'param2'], 32 | }, 33 | /** 34 | * array-params with new baseUrl 35 | */ 36 | { 37 | name: 'hap', 38 | path: 'array-params', 39 | /** @type { import('../../src/').ReqType } */ 40 | reqType: ('axios'), 41 | middleware: [ 42 | async (ctx, next) => { 43 | ctx.req.baseUrl = 'http://custom-baseUrl.com/' 44 | await next() 45 | }, 46 | ], 47 | }, 48 | /** 49 | * object-params 50 | */ 51 | { 52 | name: 'op', 53 | path: 'object-params', 54 | params: { 55 | param1: 1217, 56 | param2: 'steve', 57 | param3: { isRequired: true }, 58 | }, 59 | }, 60 | /** 61 | * own-baseUrl 62 | */ 63 | { 64 | name: 'oh', 65 | path: 'own-baseUrl', 66 | baseUrl: 'http://example-test.com/', 67 | params: {}, 68 | // 表示这个接口不需要传递 commonParams 69 | commonParams: null, 70 | }, 71 | /** 72 | * custom-transformRequest 73 | */ 74 | { 75 | name: 'ct', 76 | path: 'custom-transformRequest', 77 | axiosOptions: { 78 | transformRequest: () => 'ct', 79 | }, 80 | }, 81 | /** 82 | * application/json 83 | */ 84 | { name: 'pj', path: 'post-json' }, 85 | /** 86 | * raw-data 87 | */ 88 | { 89 | name: 'rd', 90 | path: 'raw-data', 91 | afterFn: ([, ctx]) => ctx.res.rawData, 92 | }, 93 | ], 94 | } 95 | -------------------------------------------------------------------------------- /examples/apis-web/index.d.ts: -------------------------------------------------------------------------------- 1 | // default response result 2 | interface Result { code: number, data: any, msg?: string } 3 | interface ReqFn { 4 | key: string 5 | mock: any 6 | params: object | string[] 7 | } 8 | interface RuntimeOptions { 9 | // for jsonp 10 | callbackName?: string 11 | [key: string]: any 12 | } 13 | interface ReqFnWithAnyParams extends ReqFn { 14 | (params?: any, options?: RuntimeOptions): Promise 15 | } 16 | 17 | export const fakeGetApi: { 18 | 'acp': ReqFnWithAnyParams 19 | 'rta': ReqFnWithAnyParams 20 | 'irt': ReqFnWithAnyParams 21 | 'afterData': ReqFnWithAnyParams 22 | 'mockFnData': ReqFnWithAnyParams 23 | 'noAfterData': ReqFnWithAnyParams 24 | 'jsonpOptions': ReqFnWithAnyParams 25 | 'beforeFnCookie': ReqFnWithAnyParams 26 | 'mockObjectData': ReqFnWithAnyParams 27 | 'empty-array-params': ReqFnWithAnyParams 28 | 'ap': ReqFn & { 29 | ( 30 | params: { param1?: any, param2?: any }, 31 | options?: RuntimeOptions 32 | ): Promise 33 | } 34 | 'op': ReqFn & { 35 | ( 36 | params: { param1?: any, param2?: any, param3: any }, 37 | options?: RuntimeOptions 38 | ): Promise 39 | } 40 | } 41 | 42 | export const fakePostApi: { 43 | 'ct': ReqFnWithAnyParams 44 | 'oh': ReqFnWithAnyParams 45 | 'pj': ReqFnWithAnyParams 46 | 'eap': ReqFnWithAnyParams 47 | 'hap': ReqFnWithAnyParams 48 | 'ap': ReqFn & { 49 | ( 50 | params: { param1?: any, param2?: any }, 51 | options?: RuntimeOptions 52 | ): Promise 53 | } 54 | 'op': ReqFn & { 55 | ( 56 | params: { param1?: any, param2?: any, param3: any }, 57 | options?: RuntimeOptions 58 | ): Promise 59 | } 60 | } 61 | 62 | export const fakeFnApi: { 63 | 'fp': ReqFn & { 64 | ( 65 | params: { t?: any }, 66 | options?: RuntimeOptions 67 | ): Promise 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/apis-web/index.js: -------------------------------------------------------------------------------- 1 | import TuaApi from '../../src' 2 | 3 | const tuaApi = new TuaApi({ 4 | host: 'http://example-base.com/', 5 | // 默认用 jsonp 的方式,不填默认用 axios 6 | reqType: 'jsonp', 7 | }) 8 | 9 | // 使用中间件 10 | tuaApi.use(async (ctx, next) => { 11 | // 请求发起前 12 | // console.log('before: ', ctx) 13 | 14 | await next() 15 | 16 | // 响应返回后 17 | // console.log('after: ', ctx) 18 | }) 19 | 20 | export const fakeFnApi = tuaApi.getApi(require('./fake-fn').default) 21 | export const fakeGetApi = tuaApi.getApi(require('./fake-get').default) 22 | export const fakePostApi = tuaApi.getApi(require('./fake-post').default) 23 | -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | interface Wx { 2 | request: jest.Mocked 3 | hideLoading: jest.Mocked 4 | showLoading: jest.Mocked 5 | hideNavigationBarLoading: jest.Mocked 6 | showNavigationBarLoading: jest.Mocked 7 | 8 | // just for test 9 | __TEST_DATA__: { 10 | testData?: any 11 | isTestFail?: boolean 12 | } 13 | } 14 | 15 | declare const wx: Wx 16 | 17 | declare namespace NodeJS { 18 | interface Global { 19 | wx: Wx 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bail: true, 3 | clearMocks: true, 4 | transform: { 5 | '^.+\\.js$': 'babel-jest', 6 | }, 7 | moduleNameMapper: { 8 | '@/(.*)$': '/src/$1', 9 | '@examples/(.*)$': '/examples/$1', 10 | }, 11 | collectCoverage: true, 12 | collectCoverageFrom: [ 13 | 'src/**', 14 | '!src/index.d.ts', 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tua-api", 3 | "version": "1.7.0", 4 | "description": "🏗 A common tool helps converting configs to api functions", 5 | "main": "dist/TuaApi.cjs.js", 6 | "module": "dist/TuaApi.esm.js", 7 | "unpkg": "dist/TuaApi.umd.js", 8 | "jsdelivr": "dist/TuaApi.umd.js", 9 | "types": "src/index.d.ts", 10 | "files": [ 11 | "src", 12 | "dist", 13 | "examples" 14 | ], 15 | "scripts": { 16 | "cov": "open coverage/lcov-report/index.html", 17 | "docs": "vuepress dev docs", 18 | "docs:build": "vuepress build docs", 19 | "lint": "eslint --fix . docs/.vuepress/ --ignore-path .gitignore", 20 | "test": "cross-env NODE_ENV=test jest", 21 | "test:tdd": "cross-env NODE_ENV=test jest --watch", 22 | "prebuild": "rimraf dist/* & npm run test", 23 | "build": "cross-env NODE_ENV=production rollup -c", 24 | "deploy": "npm run docs:build && gh-pages -m \"[ci skip]\" -d docs/.vuepress/dist", 25 | "next:pm": "npm --no-git-tag-version version preminor", 26 | "next:pr": "npm --no-git-tag-version version prerelease", 27 | "pub": "npm run build && npm publish", 28 | "pub:n": "npm run build && npm publish --tag next" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-push": "npm test", 33 | "pre-commit": "lint-staged", 34 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 35 | } 36 | }, 37 | "lint-staged": { 38 | "{src,test}/**/*.js": [ 39 | "eslint --fix" 40 | ] 41 | }, 42 | "eslintIgnore": [ 43 | "dist/*", 44 | "!.eslintrc.js", 45 | "package.json" 46 | ], 47 | "dependencies": { 48 | "axios": "^0.21.1", 49 | "fetch-jsonp": "^1.1.3", 50 | "koa-compose": "^4.1.0" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.10.5", 54 | "@babel/plugin-external-helpers": "^7.10.4", 55 | "@babel/plugin-proposal-decorators": "^7.10.5", 56 | "@babel/plugin-proposal-object-rest-spread": "^7.10.4", 57 | "@babel/preset-env": "^7.10.4", 58 | "@commitlint/cli": "^9.1.1", 59 | "@commitlint/config-conventional": "^9.1.1", 60 | "@rollup/plugin-babel": "^5.1.0", 61 | "@rollup/plugin-commonjs": "^14.0.0", 62 | "@rollup/plugin-json": "^4.1.0", 63 | "@rollup/plugin-node-resolve": "^8.4.0", 64 | "@rollup/plugin-replace": "^2.3.3", 65 | "@types/jest": "^26.0.5", 66 | "all-contributors-cli": "^6.16.1", 67 | "axios-mock-adapter": "^1.18.2", 68 | "babel-core": "^7.0.0-bridge.0", 69 | "babel-eslint": "^10.1.0", 70 | "babel-jest": "^26.1.0", 71 | "codecov": "^3.7.1", 72 | "cross-env": "^7.0.2", 73 | "eslint": "^7.5.0", 74 | "eslint-config-standard": "^14.1.1", 75 | "eslint-plugin-import": "^2.22.0", 76 | "eslint-plugin-node": "^11.1.0", 77 | "eslint-plugin-promise": "^4.2.1", 78 | "eslint-plugin-standard": "^4.0.1", 79 | "gh-pages": "^3.1.0", 80 | "husky": "^4.2.5", 81 | "jest": "^26.1.0", 82 | "lint-staged": "^10.2.11", 83 | "rimraf": "^3.0.2", 84 | "rollup": "^2.22.1", 85 | "rollup-plugin-eslint": "^7.0.0", 86 | "rollup-plugin-terser": "^6.1.0", 87 | "typescript": "^3.9.7", 88 | "vuepress": "^1.5.2" 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/tuateam/tua-api.git" 93 | }, 94 | "homepage": "https://tuateam.github.io/tua-api/", 95 | "author": "StEve Young", 96 | "license": "MIT" 97 | } 98 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from '@rollup/plugin-json' 2 | import babel from '@rollup/plugin-babel' 3 | import replace from '@rollup/plugin-replace' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import { eslint } from 'rollup-plugin-eslint' 6 | import { terser } from 'rollup-plugin-terser' 7 | import nodeResolve from '@rollup/plugin-node-resolve' 8 | 9 | import pkg from './package.json' 10 | 11 | const input = 'src/index.js' 12 | const banner = `/* ${pkg.name} version ${pkg.version} */` 13 | 14 | const output = { 15 | cjs: { 16 | file: pkg.main, 17 | banner, 18 | format: 'cjs', 19 | exports: 'named', 20 | }, 21 | esm: { 22 | file: pkg.module, 23 | banner, 24 | format: 'esm', 25 | }, 26 | umd: { 27 | file: pkg.unpkg, 28 | name: 'TuaApi', 29 | banner, 30 | format: 'umd', 31 | exports: 'named', 32 | globals: { 33 | axios: 'axios', 34 | 'fetch-jsonp': 'fetchJsonp', 35 | }, 36 | }, 37 | } 38 | const plugins = [ 39 | eslint(), 40 | json(), 41 | nodeResolve(), 42 | commonjs(), 43 | babel({ babelHelpers: 'bundled' }), 44 | ] 45 | const env = 'process.env.NODE_ENV' 46 | const external = ['axios', 'fetch-jsonp'] 47 | 48 | export default [{ 49 | input, 50 | output: [output.cjs, output.esm], 51 | plugins, 52 | external, 53 | }, { 54 | input, 55 | output: output.umd, 56 | external, 57 | plugins: [ 58 | ...plugins, 59 | replace({ [env]: '"development"' }), 60 | ], 61 | }, { 62 | input, 63 | output: { 64 | ...output.umd, 65 | file: 'dist/TuaApi.umd.min.js', 66 | }, 67 | external, 68 | plugins: [ 69 | ...plugins, 70 | replace({ [env]: '"production"' }), 71 | terser({ 72 | output: { 73 | /* eslint-disable */ 74 | ascii_only: true, 75 | }, 76 | }), 77 | ], 78 | }] 79 | -------------------------------------------------------------------------------- /src/adapters/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { DEFAULT_HEADER } from '../constants' 4 | import { logger, isFormData, isUndefined, getParamStrFromObj } from '../utils' 5 | 6 | /** 7 | * 获取使用 axios 发起请求后的 promise 对象 8 | * @param {object} options 9 | */ 10 | export const getAxiosPromise = ({ 11 | url, 12 | data, 13 | method, 14 | headers, 15 | crossDomain = true, 16 | withCredentials = true, 17 | transformRequest, 18 | ...rest 19 | }) => { 20 | const isFD = isFormData(data) 21 | const isPost = method.toLowerCase() === 'post' 22 | 23 | logger.log(`Req Url: ${url}`) 24 | if (data && (Object.keys(data).length || isFD)) { 25 | logger.log('Req Data:', data) 26 | } 27 | 28 | // 优先使用用户的配置 29 | if (isUndefined(transformRequest)) { 30 | transformRequest = isFD 31 | ? null 32 | : isPost 33 | // 如果使用 post 的请求方式,自动对其 stringify 34 | ? x => JSON.stringify(x) 35 | : getParamStrFromObj 36 | } 37 | if (isUndefined(headers)) { 38 | headers = isPost 39 | ? { 'Content-Type': 'application/json;charset=utf-8' } 40 | : DEFAULT_HEADER 41 | } 42 | 43 | return axios({ 44 | url, 45 | data, 46 | method, 47 | headers, 48 | crossDomain, 49 | withCredentials, 50 | transformRequest, 51 | ...rest, 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /src/adapters/index.js: -------------------------------------------------------------------------------- 1 | export { getWxPromise } from './wx' 2 | export { getAxiosPromise } from './axios' 3 | export { getFetchJsonpPromise } from './jsonp' 4 | -------------------------------------------------------------------------------- /src/adapters/jsonp.js: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils' 2 | 3 | const fetchJsonp = require('fetch-jsonp') 4 | 5 | // 获取发起 jsonp 请求后的 promise 对象 6 | export const getFetchJsonpPromise = ({ url, jsonpOptions }) => { 7 | logger.log(`Jsonp Url: ${url}`) 8 | 9 | return fetchJsonp(url, jsonpOptions) 10 | .then(res => res.json()) 11 | .then(data => ({ data })) 12 | } 13 | -------------------------------------------------------------------------------- /src/adapters/wx.js: -------------------------------------------------------------------------------- 1 | import { logger, promisifyWxApi } from '../utils' 2 | import { ERROR_STRINGS, WX_VALID_METHODS } from '../constants' 3 | 4 | /** 5 | * 获取使用 wx 发起请求后的 promise 对象 6 | * @param {object} options 7 | */ 8 | export const getWxPromise = ({ 9 | url, 10 | data, 11 | method, 12 | header, 13 | fullUrl, 14 | isShowLoading = true, 15 | showLoadingFn = () => wx.showLoading({ title: '加载中' }), 16 | hideLoadingFn = wx.hideLoading.bind(wx), 17 | ...rest 18 | }) => { 19 | method = method.toUpperCase() 20 | 21 | if (method === 'GET') { 22 | logger.log(`Req Url: ${fullUrl}`) 23 | } else { 24 | logger.log(`Req Url: ${url}`) 25 | if (data && Object.keys(data).length) { 26 | logger.log('Req Data:', data) 27 | } 28 | } 29 | 30 | // 展示 loading 31 | isShowLoading && showLoadingFn() 32 | 33 | if (WX_VALID_METHODS.indexOf(method) === -1) { 34 | return Promise.reject(Error(ERROR_STRINGS.unknownMethodFn(method))) 35 | } 36 | 37 | return promisifyWxApi(wx.request)({ 38 | ...rest, 39 | url, 40 | data, 41 | header, 42 | method, 43 | complete: () => { 44 | // 同步隐藏 loading 45 | isShowLoading && hideLoadingFn() 46 | /* istanbul ignore next */ 47 | rest.complete && rest.complete() 48 | }, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // 支持的请求类型 2 | const VALID_REQ_TYPES = ['wx', 'axios', 'jsonp', 'custom'] 3 | 4 | // 小程序中合法的请求方法 5 | const WX_VALID_METHODS = ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT'] 6 | 7 | // 默认请求头 8 | const DEFAULT_HEADER = { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' } 9 | 10 | // 错误信息 11 | const ERROR_STRINGS = { 12 | noData: 'no data!', 13 | argsType: 'the first parameter must be an object!', 14 | middleware: 'middleware must be a function!', 15 | reqTypeAndCustomFetch: 'reqType or customFetch only!', 16 | 17 | reqTypeFn: (reqType) => `invalid reqType: "${reqType}", ` + 18 | `support these reqTypes: ["${VALID_REQ_TYPES.join('", "')}"].`, 19 | unknownMethodFn: method => `unknown method: "${method}"!`, 20 | requiredParamFn: (apiName, param) => `${apiName} must pass required param: "${param}"!`, 21 | } 22 | 23 | export { 24 | ERROR_STRINGS, 25 | DEFAULT_HEADER, 26 | VALID_REQ_TYPES, 27 | WX_VALID_METHODS, 28 | } 29 | -------------------------------------------------------------------------------- /src/exportUtils.js: -------------------------------------------------------------------------------- 1 | import { 2 | map, 3 | pipe, 4 | filter, 5 | values, 6 | flatten, 7 | mergeAll, 8 | } from './utils/' 9 | 10 | /** 11 | * 将各个发起 api 的函数的 key 与其绑定,与 TuaStorage 配合使用效果更佳 12 | * apis: { api1: apiMap1, api2: apiMap2 } 13 | * apiMap: { 14 | * path1: { [Function: path1] key: key1 }, 15 | * path2: { [Function: path2] key: key2 }, 16 | * ... 17 | * } 18 | * 19 | * 转换成 { 20 | * key1: [Function: path1] key: key1, 21 | * key2: [Function: path2] key: key2, 22 | * ... 23 | * } 24 | * @param {object} apis 25 | * @return {object} 26 | */ 27 | const getSyncFnMapByApis = pipe( 28 | values, 29 | map(values), 30 | flatten, 31 | map(val => ({ [val.key]: val })), 32 | mergeAll, 33 | ) 34 | 35 | /** 36 | * 过滤出有默认参数的接口(接口参数非数组,且不含有 isRequired/required) 37 | * @param {object} syncFnMap 38 | * @return {Array} keys 所有有默认参数的接口名称 39 | */ 40 | const getPreFetchFnKeysBySyncFnMap = (syncFnMap) => pipe( 41 | Object.keys, 42 | filter((key) => { 43 | const { params } = syncFnMap[key] 44 | 45 | if (Array.isArray(params)) return false 46 | 47 | // 当前参数不是必须的 48 | const isParamNotRequired = (key) => ( 49 | typeof params[key] !== 'object' || 50 | // 兼容 vue 的写法 51 | (!params[key].isRequired && !params[key].required) 52 | ) 53 | 54 | return Object.keys(params).every(isParamNotRequired) 55 | }), 56 | map(key => ({ key })), 57 | )(syncFnMap) 58 | 59 | export { 60 | getSyncFnMapByApis, 61 | getPreFetchFnKeysBySyncFnMap, 62 | } 63 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Options as JsonpOptions } from 'fetch-jsonp' 2 | import { 3 | AxiosResponse, 4 | AxiosRequestConfig as AxiosOptions, 5 | } from 'axios' 6 | 7 | /* -- types -- */ 8 | export type AnyFunction = (...args: any[]) => any 9 | export type AnyPromiseFunction = (...args: any[]) => Promise 10 | 11 | export type Mock = AnyFunction | any 12 | 13 | export type ApiConfig = WxApiConfig | WebApiConfig 14 | 15 | export type ParamsConfig = string[] | ParamsObject | ((args?: object) => object) 16 | 17 | export type Middleware = (ctx: T, next: () => Promise) => Promise 18 | 19 | export type RuntimeOptions = WxRuntimeOptions | WebRuntimeOptions 20 | 21 | export type ReqType = ( 22 | | 'wx' | 'WX' 23 | | 'axios' | 'AXIOS' 24 | | 'jsonp' | 'JSONP' 25 | | 'custom' | 'CUSTOM' 26 | ) 27 | 28 | export type Method = ( 29 | | 'get' | 'GET' 30 | | 'put' | 'PUT' 31 | | 'head' | 'HEAD' 32 | | 'post' | 'POST' 33 | | 'trace' | 'TRACE' 34 | | 'delete' | 'DELETE' 35 | | 'connect' | 'CONNECT' 36 | | 'options' | 'OPTIONS' 37 | ) 38 | 39 | /* -- interfaces -- */ 40 | 41 | export interface ParamsObject { 42 | [k: string]: ( 43 | | { required: boolean } 44 | | { isRequired: boolean } 45 | | any 46 | ) 47 | } 48 | 49 | export interface CtxReq { 50 | // deprecated 51 | host: string 52 | baseUrl: string 53 | // deprecated 54 | type: Method 55 | method: Method 56 | 57 | mock: Mock 58 | path: string 59 | prefix: string 60 | reqType: ReqType 61 | reqParams: object 62 | reqFnParams: object 63 | callbackName: string 64 | axiosOptions: AxiosOptions 65 | jsonpOptions: JsonpOptions 66 | [k: string]: any 67 | } 68 | export interface CtxRes extends AxiosResponse { 69 | data: any 70 | rawData: any 71 | error?: Error 72 | [k: string]: any 73 | } 74 | 75 | export interface Ctx { 76 | req: CtxReq 77 | res: CtxRes 78 | endTime: number 79 | reqTime: number 80 | startTime: number 81 | [k: string]: any 82 | } 83 | 84 | export interface BaseApiConfig { 85 | // deprecated 86 | host?: string 87 | baseUrl?: string 88 | // deprecated 89 | type?: Method 90 | method?: Method 91 | 92 | mock?: Mock 93 | prefix?: string 94 | reqType?: ReqType 95 | afterFn?: (args: [U?, Ctx?]) => Promise 96 | beforeFn?: () => Promise 97 | middleware?: Middleware[] 98 | customFetch?: AnyPromiseFunction 99 | commonParams?: object | ((args?: object) => object), 100 | axiosOptions?: AxiosOptions 101 | jsonpOptions?: JsonpOptions 102 | useGlobalMiddleware?: boolean 103 | [k: string]: any 104 | } 105 | 106 | // for web 107 | export interface WebApiConfig extends BaseApiConfig { 108 | pathList: (BaseApiConfig & { 109 | path: string 110 | name?: string 111 | params?: ParamsConfig 112 | })[] 113 | } 114 | 115 | // for wechat miniprogram 116 | export interface WxApiConfig extends WebApiConfig { 117 | isShowLoading?: boolean 118 | showLoadingFn?: AnyFunction 119 | hideLoadingFn?: AnyFunction 120 | } 121 | 122 | export interface RuntimeOptionsOnly { 123 | apiName?: string 124 | fullPath?: string 125 | callbackName?: string 126 | } 127 | export interface WxRuntimeOptions extends WxApiConfig, RuntimeOptionsOnly { } 128 | export interface WebRuntimeOptions extends WebApiConfig, RuntimeOptionsOnly { } 129 | 130 | export interface Api { 131 | key: string 132 | mock: Mock 133 | params: ParamsConfig 134 | ( 135 | params?: U, 136 | runtimeOptions?: RuntimeOptions 137 | ): Promise 138 | } 139 | export interface Apis { [k: string]: SyncFnMap } 140 | export interface SyncFnMap { [k: string]: Api } 141 | 142 | export interface TuaApiClass { 143 | new(args?: { 144 | // deprecated 145 | host?: string 146 | baseUrl?: string 147 | 148 | reqType?: string 149 | middleware?: Middleware[] 150 | customFetch?: AnyPromiseFunction 151 | axiosOptions?: AxiosOptions 152 | jsonpOptions?: JsonpOptions 153 | defaultErrorData?: any 154 | }): TuaApiInstance 155 | } 156 | 157 | export interface TuaApiInstance { 158 | use: (fn: Middleware) => TuaApiInstance 159 | getApi: (apiConfig: ApiConfig) => SyncFnMap 160 | } 161 | 162 | /* -- export utils -- */ 163 | 164 | export function getSyncFnMapByApis (apis: Apis): SyncFnMap 165 | export function getPreFetchFnKeysBySyncFnMap (syncFnMap: SyncFnMap): Api[] 166 | 167 | /* -- export default -- */ 168 | 169 | declare const TuaApi: TuaApiClass 170 | export default TuaApi 171 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import koaCompose from 'koa-compose' 3 | 4 | import { version } from '../package.json' 5 | import { 6 | map, 7 | pipe, 8 | isWx, 9 | runFn, 10 | logger, 11 | mergeAll, 12 | apiConfigToReqFnParams, 13 | } from './utils' 14 | import { 15 | ERROR_STRINGS, 16 | VALID_REQ_TYPES, 17 | } from './constants' 18 | import { 19 | getWxPromise, 20 | getAxiosPromise, 21 | getFetchJsonpPromise, 22 | } from './adapters/' 23 | import { 24 | formatResDataMiddleware, 25 | recordReqTimeMiddleware, 26 | setReqFnParamsMiddleware, 27 | recordStartTimeMiddleware, 28 | formatReqParamsMiddleware, 29 | } from './middlewareFns' 30 | 31 | logger.log(`Version: ${version}`) 32 | 33 | class TuaApi { 34 | /** 35 | * @param {object} [options] 36 | * @param {string} [options.host] 服务器基础地址,例如 https://example.com/ 37 | * @param {string} [options.baseUrl] 服务器基础地址,例如 https://example.com/ 38 | * @param {string} [options.reqType] 使用什么工具发(axios/jsonp/wx) 39 | * @param {function[]} [options.middleware] 中间件函数数组 40 | * @param {function} [options.customFetch] 自定义请求函数 41 | * @param {object} [options.axiosOptions] 透传 axios 配置参数 42 | * @param {object} [options.jsonpOptions] 透传 fetch-jsonp 配置参数 43 | * @param {object} [options.defaultErrorData] 出错时的默认数据 44 | */ 45 | constructor ({ 46 | host, 47 | baseUrl = host, 48 | reqType, 49 | middleware = [], 50 | customFetch, 51 | axiosOptions = {}, 52 | jsonpOptions = {}, 53 | defaultErrorData = { code: 999, msg: '出错啦!' }, 54 | } = {}) { 55 | this.baseUrl = baseUrl 56 | this.reqType = reqType !== undefined 57 | ? reqType.toLowerCase() 58 | : (isWx() ? 'wx' : 'axios') 59 | this.middleware = middleware 60 | this.customFetch = customFetch 61 | this.axiosOptions = axiosOptions 62 | this.jsonpOptions = jsonpOptions 63 | this.defaultErrorData = defaultErrorData 64 | 65 | this._checkReqType(this.reqType) 66 | 67 | if (host) { 68 | logger.warn( 69 | '[host] will be deprecated, please use [baseUrl] instead!\n' + 70 | '[host] 属性将被废弃, 请用 [baseUrl] 替代!', 71 | ) 72 | } 73 | if (reqType && reqType !== 'custom' && customFetch) { 74 | throw TypeError(ERROR_STRINGS.reqTypeAndCustomFetch) 75 | } 76 | 77 | return this 78 | } 79 | 80 | /* -- 各种对外暴露方法 -- */ 81 | 82 | /** 83 | * 添加一个中间件函数 84 | * @param {function} fn 85 | * @return {object} self 86 | */ 87 | use (fn) { 88 | if (typeof fn !== 'function') { 89 | throw TypeError(ERROR_STRINGS.middleware) 90 | } 91 | this.middleware.push(fn) 92 | 93 | return this 94 | } 95 | 96 | /** 97 | * 根据 apiConfig 生成请求函数组成的 map 98 | * @param {object} apiConfig 99 | * @return {object} 100 | */ 101 | getApi (apiConfig) { 102 | return pipe( 103 | apiConfigToReqFnParams, 104 | map(this._getOneReqMap.bind(this)), 105 | mergeAll, 106 | )(apiConfig) 107 | } 108 | 109 | /* -- 各种私有方法 -- */ 110 | 111 | /** 112 | * 根据 reqType 和 type 决定调用哪个库 113 | * @param {object} options 114 | * @param {Object|Function} options.mock 模拟的响应数据或是生成数据的函数 115 | * @param {string} options.url 接口地址 116 | * @param {string} options.method 接口请求类型 get/post... 117 | * @param {string} options.fullUrl 完整接口地址 118 | * @param {string} options.reqType 使用什么工具发(axios/jsonp/wx) 119 | * @param {object} options.reqParams 请求参数 120 | * @param {object} options.header 请求的 header 121 | * @param {function} [options.customFetch] 自定义请求函数 122 | * @param {string} options.callback 使用 jsonp 时标识回调函数的名称 123 | * @param {string} options.callbackName 使用 jsonp 时的回调函数名 124 | * @param {object} options.axiosOptions 透传 axios 配置参数 125 | * @param {object} options.jsonpOptions 透传 fetch-jsonp 配置参数 126 | * @return {Promise} 127 | */ 128 | _reqFn (options) { 129 | const { 130 | url, 131 | mock, 132 | header, 133 | method: _method, 134 | fullUrl, 135 | reqType: _reqType, 136 | reqParams: data, 137 | callback, 138 | callbackName, 139 | axiosOptions, 140 | jsonpOptions, 141 | ...rest 142 | } = options 143 | 144 | // check type 145 | this._checkReqType(_reqType) 146 | 147 | // mock data 148 | if (mock) { 149 | const resData = { ...runFn(mock, data) } 150 | 151 | return Promise.resolve({ data: resData }) 152 | } 153 | 154 | const method = _method.toLowerCase() 155 | const reqType = _reqType.toLowerCase() 156 | 157 | if (reqType === 'custom') { 158 | return rest.customFetch({ url, data, method, header, ...rest }) 159 | } 160 | 161 | if (reqType === 'wx') { 162 | return getWxPromise({ url, fullUrl, data, method, header, ...rest }) 163 | } 164 | 165 | if (reqType === 'axios' || method === 'post') { 166 | const params = { 167 | ...axiosOptions, 168 | url: method === 'get' ? fullUrl : url, 169 | data: method === 'get' ? {} : data, 170 | method, 171 | headers: header, 172 | } 173 | 174 | return getAxiosPromise(params) 175 | } 176 | 177 | // 防止接口返回非英文时报错 178 | jsonpOptions.charset = jsonpOptions.charset || 'UTF-8' 179 | jsonpOptions.jsonpCallback = callback || jsonpOptions.jsonpCallback 180 | jsonpOptions.jsonpCallbackFunction = callbackName || jsonpOptions.jsonpCallbackFunction 181 | 182 | return getFetchJsonpPromise({ url: fullUrl, jsonpOptions }) 183 | } 184 | 185 | /** 186 | * 检查 reqType 是否合法 187 | */ 188 | _checkReqType (reqType) { 189 | if (VALID_REQ_TYPES.indexOf(reqType) !== -1) return 190 | 191 | throw TypeError(ERROR_STRINGS.reqTypeFn(reqType)) 192 | } 193 | 194 | /** 195 | * 组合生成中间件函数 196 | * @param {function[]} middleware 197 | * @param {Boolean} useGlobalMiddleware 是否使用全局中间件 198 | */ 199 | _getMiddlewareFn (middleware, useGlobalMiddleware) { 200 | const middlewareFns = useGlobalMiddleware 201 | ? this.middleware.concat(middleware) 202 | : middleware 203 | 204 | return koaCompose([ 205 | // 记录开始时间 206 | recordStartTimeMiddleware, 207 | // 格式化生成请求参数 208 | formatReqParamsMiddleware, 209 | // 业务侧中间件函数数组 210 | ...middlewareFns, 211 | // 生成 _reqFn 参数 212 | setReqFnParamsMiddleware, 213 | // 统一转换响应数据为对象 214 | formatResDataMiddleware, 215 | // 记录结束时间 216 | recordReqTimeMiddleware, 217 | // 发起请求 218 | (ctx, next) => next() 219 | .then(() => this._reqFn(ctx.req.reqFnParams)) 220 | // 暂存出错,保证 afterFn 能执行(finally) 221 | .catch((error) => ({ 222 | // 浅拷贝一份默认出错值 223 | data: { ...this.defaultErrorData }, 224 | error, 225 | })) 226 | .then(res => { ctx.res = res }), 227 | ]) 228 | } 229 | 230 | /** 231 | * 接受 api 对象,返回待接收参数的单个 api 函数的对象 232 | * @param {object} options 233 | * @param {string} options.type 接口请求类型 get/post... 234 | * @param {string} options.method 接口请求类型 get/post... 235 | * @param {Object|Function} options.mock 模拟的响应数据或是生成数据的函数 236 | * @param {string} options.name 自定义的接口名称 237 | * @param {string} options.path 接口结尾路径 238 | * @param {String[] | object} options.params 接口参数数组 239 | * @param {string} options.prefix 接口前缀 240 | * @param {function} options.afterFn 在请求完成后执行的钩子函数(将被废弃) 241 | * @param {function} options.beforeFn 在请求发起前执行的钩子函数(将被废弃) 242 | * @param {function[]} options.middleware 中间件函数数组 243 | * @param {function} [options.customFetch] 自定义请求函数 244 | * @param {Boolean} options.useGlobalMiddleware 是否使用全局中间件 245 | * @param {string} options.baseUrl 服务器地址 246 | * @param {string} options.reqType 使用什么工具发 247 | * @param {object} options.axiosOptions 透传 axios 配置参数 248 | * @param {object} options.jsonpOptions 透传 fetch-jsonp 配置参数 249 | * @return {object} 以 apiName 为 key,请求函数为值的对象 250 | */ 251 | _getOneReqMap ({ 252 | type, 253 | method = type, 254 | mock, 255 | name, 256 | path, 257 | params: rawParams = {}, 258 | prefix, 259 | afterFn = ([x]) => x, 260 | beforeFn = Promise.resolve.bind(Promise), 261 | middleware = [], 262 | useGlobalMiddleware = true, 263 | ...rest 264 | }) { 265 | if (type) { 266 | logger.warn( 267 | '[type] will be deprecated, please use [method] instead!\n' + 268 | '[type] 属性将被废弃, 请用 [method] 替代!', 269 | ) 270 | } 271 | 272 | // 优先使用 name 273 | const apiName = name || path 274 | // 默认值 275 | method = method || 'get' 276 | // 向前兼容 277 | type = method 278 | 279 | /* 合并全局默认值 */ 280 | if (rest.reqType && rest.customFetch) { 281 | if (rest.reqType.toLowerCase() !== 'custom') { 282 | logger.warn(ERROR_STRINGS.reqTypeAndCustomFetch) 283 | } 284 | rest.reqType = 'custom' 285 | } else if (rest.customFetch || this.customFetch) { 286 | // 没有配置 reqType,但配了公共配置或默认配置的 customFetch 287 | rest.reqType = 'custom' 288 | } else { 289 | // 没有配置 customFetch 290 | rest.reqType = rest.reqType || this.reqType 291 | } 292 | rest.baseUrl = rest.baseUrl || this.baseUrl 293 | rest.customFetch = rest.customFetch || this.customFetch 294 | rest.axiosOptions = rest.axiosOptions 295 | ? { ...this.axiosOptions, ...rest.axiosOptions } 296 | : this.axiosOptions 297 | rest.jsonpOptions = rest.jsonpOptions 298 | ? { ...this.jsonpOptions, ...rest.jsonpOptions } 299 | : this.jsonpOptions 300 | 301 | /** 302 | * 被业务侧调用的函数 303 | * @param {object} args 接口参数(覆盖默认值) 304 | * @param {object} runtimeOptions 运行时配置 305 | * @return {Promise} 306 | */ 307 | const apiFn = (args, runtimeOptions = {}) => { 308 | args = args || {} 309 | 310 | const params = runFn(rawParams, args) 311 | 312 | // 最终的运行时配置,runtimeOptions 有最高优先级 313 | const runtimeParams = { 314 | type, 315 | path, 316 | params, 317 | prefix, 318 | apiName, 319 | fullPath: `${prefix}/${path}`, 320 | ...rest, 321 | ...runtimeOptions, 322 | } 323 | 324 | // 向前兼容 325 | runtimeParams.host = runtimeParams.host || runtimeParams.baseUrl 326 | runtimeParams.method = runtimeParams.method || runtimeParams.type 327 | runtimeParams.baseUrl = runtimeParams.baseUrl || runtimeParams.host 328 | 329 | // 请求的上下文信息 330 | const ctx = { 331 | req: { args, mock: apiFn.mock, reqFnParams: {}, ...runtimeParams }, 332 | } 333 | 334 | // 执行完 beforeFn 后执行的函数 335 | const beforeFnCb = (rArgs = {}) => { 336 | // 传递请求头 337 | if (rArgs.header) { 338 | ctx.req.reqFnParams.header = rArgs.header 339 | } 340 | 341 | if (!rArgs.params) return 342 | 343 | // 合并 beforeFn 中传入的 params 344 | ctx.req.params = Array.isArray(params) 345 | ? rArgs.params 346 | // 可以通过给 beforeFn 添加 params 返回值来添加通用参数 347 | : { ...params, ...rArgs.params } 348 | } 349 | 350 | // 中间件函数 351 | const middlewareFn = this._getMiddlewareFn(middleware, useGlobalMiddleware) 352 | 353 | return beforeFn() 354 | .then(beforeFnCb) 355 | .then(() => middlewareFn(ctx)) 356 | .then(() => afterFn([ctx.res.data, ctx])) 357 | .then((data) => ctx.res.error 358 | ? Promise.reject(ctx.res.error) 359 | : data || ctx.res.data, 360 | ) 361 | } 362 | 363 | apiFn.key = `${prefix}/${apiName}` 364 | apiFn.mock = mock 365 | apiFn.params = rawParams 366 | 367 | return { [apiName]: apiFn } 368 | } 369 | } 370 | 371 | export default TuaApi 372 | export * from './exportUtils' 373 | -------------------------------------------------------------------------------- /src/middlewareFns.js: -------------------------------------------------------------------------------- 1 | import { ERROR_STRINGS } from './constants' 2 | import { 3 | runFn, 4 | isFormData, 5 | combineUrls, 6 | checkArrayParams, 7 | getParamStrFromObj, 8 | getDefaultParamObj, 9 | } from './utils' 10 | 11 | /** 12 | * 记录请求开始时间 13 | * @param {object} ctx 上下文对象 14 | * @param {Function} next 转移控制权给下一个中间件的函数 15 | */ 16 | const recordStartTimeMiddleware = (ctx, next) => { 17 | ctx.startTime = Date.now() 18 | 19 | return next() 20 | } 21 | 22 | /** 23 | * 记录接受响应时间和请求总时间的中间件 24 | * @param {object} ctx 上下文对象 25 | * @param {Function} next 转移控制权给下一个中间件的函数 26 | */ 27 | const recordReqTimeMiddleware = (ctx, next) => { 28 | return next().then(() => { 29 | ctx.endTime = Date.now() 30 | ctx.reqTime = Date.now() - ctx.startTime 31 | }) 32 | } 33 | 34 | /** 35 | * 由于后台返回数据结构不统一,增加对于返回数组情况的兼容处理 36 | * 且对于 code 进行强制类型转换 37 | * @param {object} ctx 上下文对象(ctx.res.data 接口返回数据) 38 | * @param {Function} next 转移控制权给下一个中间件的函数 39 | */ 40 | const formatResDataMiddleware = (ctx, next) => next().then(() => { 41 | const jsonData = ctx.res.data 42 | ctx.res.rawData = ctx.res.data 43 | 44 | if (!jsonData) return Promise.reject(Error(ERROR_STRINGS.noData)) 45 | 46 | if (Array.isArray(jsonData)) { 47 | const [code, data, msg] = jsonData 48 | ctx.res.data = { code: +code, data, msg } 49 | } else { 50 | ctx.res.data = { ...jsonData, code: +jsonData.code } 51 | } 52 | }) 53 | 54 | /** 55 | * 生成请求函数所需参数 56 | * @param {object} ctx 上下文对象 57 | * @param {Function} next 转移控制权给下一个中间件的函数 58 | */ 59 | const formatReqParamsMiddleware = (ctx, next) => { 60 | const { 61 | args, 62 | params, 63 | commonParams: rawCommonParams, 64 | } = ctx.req 65 | 66 | if (typeof args !== 'object') { 67 | throw TypeError(ERROR_STRINGS.argsType) 68 | } 69 | 70 | if (isFormData(args) || Array.isArray(args)) { 71 | ctx.req.reqParams = args 72 | 73 | return next() 74 | } 75 | 76 | checkArrayParams(ctx.req) 77 | 78 | const commonParams = runFn(rawCommonParams, args) 79 | // 根据配置生成请求的参数 80 | ctx.req.reqParams = Array.isArray(params) 81 | ? { ...commonParams, ...args } 82 | : { ...getDefaultParamObj({ ...ctx.req, commonParams }), ...args } 83 | 84 | return next() 85 | } 86 | 87 | /** 88 | * 设置请求的 reqFnParams 参数,将被用于 _reqFn 函数 89 | * @param {object} ctx 上下文对象 90 | * @param {Function} next 转移控制权给下一个中间件的函数 91 | */ 92 | const setReqFnParamsMiddleware = (ctx, next) => { 93 | const { path, prefix, reqParams, baseUrl, ...rest } = ctx.req 94 | 95 | // 请求地址 96 | const url = combineUrls(combineUrls(baseUrl, prefix), path) 97 | const paramsStr = getParamStrFromObj(reqParams) 98 | // 完整请求地址,将参数拼在 url 上,用于 get 请求 99 | const fullUrl = paramsStr ? `${url}?${paramsStr}` : url 100 | 101 | ctx.req.reqFnParams = { 102 | url, 103 | baseUrl, 104 | fullUrl, 105 | reqParams, 106 | ...rest, 107 | // 若是用户自己传递 reqFnParams 则优先级最高 108 | ...ctx.req.reqFnParams, 109 | } 110 | 111 | return next() 112 | } 113 | 114 | export { 115 | recordReqTimeMiddleware, 116 | formatResDataMiddleware, 117 | setReqFnParamsMiddleware, 118 | recordStartTimeMiddleware, 119 | formatReqParamsMiddleware, 120 | } 121 | -------------------------------------------------------------------------------- /src/utils/combineUrls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a new URL by combining the specified URLs 3 | * @param {string} baseUrl The base URL 4 | * @param {string} relativeUrl The relative URL 5 | * @returns {string} The combined URL 6 | */ 7 | function combineUrls (baseUrl = '', relativeUrl = '') { 8 | const strBaseUrl = baseUrl === null ? '' : String(baseUrl) 9 | const strRelativeUrl = relativeUrl === null ? '' : String(relativeUrl) 10 | 11 | if (!strRelativeUrl) return strBaseUrl 12 | 13 | return ( 14 | strBaseUrl.replace(/\/+$/, '') + 15 | '/' + 16 | strRelativeUrl.replace(/^\/+/, '') 17 | ) 18 | } 19 | 20 | export { 21 | combineUrls, 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/fp.js: -------------------------------------------------------------------------------- 1 | const map = (fn) => (arr) => Array.isArray(arr) 2 | ? arr.map(fn) 3 | : pipe( 4 | Object.keys, 5 | map(key => ({ [key]: fn(arr[key]) })), 6 | mergeAll, 7 | )(arr) 8 | 9 | const join = str => arr => arr.join(str) 10 | const concat = val => arr => arr.concat(val) 11 | const filter = fn => arr => arr.filter(fn) 12 | const values = obj => map(k => obj[k])(Object.keys(obj)) 13 | const reduce = (fn, val) => (arr) => !arr.length 14 | ? val 15 | : val == null 16 | ? arr.reduce(fn) 17 | : arr.reduce(fn, val) 18 | 19 | const flatten = reduce( 20 | (acc, cur) => Array.isArray(cur) 21 | ? compose(concat, flatten)(cur)(acc) 22 | : concat(cur)(acc), 23 | [], 24 | ) 25 | 26 | const merge = (acc, cur) => ({ ...acc, ...cur }) 27 | const mergeAll = reduce(merge, {}) 28 | 29 | /** 30 | * 从左向右结合函数 31 | * @param {Function[]} funcs 函数数组 32 | */ 33 | const pipe = (...funcs) => { 34 | if (funcs.length === 0) return arg => arg 35 | if (funcs.length === 1) return funcs[0] 36 | 37 | return funcs.reduce((a, b) => (...args) => b(a(...args))) 38 | } 39 | 40 | /** 41 | * 从右向左结合函数 42 | * @param {Function[]} funcs 函数数组 43 | */ 44 | const compose = (...funcs) => { 45 | if (funcs.length === 0) return arg => arg 46 | if (funcs.length === 1) return funcs[0] 47 | 48 | return funcs.reduce((a, b) => (...args) => a(b(...args))) 49 | } 50 | 51 | export { 52 | map, 53 | join, 54 | pipe, 55 | merge, 56 | concat, 57 | reduce, 58 | filter, 59 | values, 60 | compose, 61 | flatten, 62 | mergeAll, 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './fp' 2 | export * from './mp' 3 | export * from './judge' 4 | export * from './logger' 5 | export * from './params' 6 | export * from './combineUrls' 7 | -------------------------------------------------------------------------------- /src/utils/judge.js: -------------------------------------------------------------------------------- 1 | export const isWx = () => ( 2 | typeof wx !== 'undefined' && 3 | typeof wx.request === 'function' 4 | ) 5 | 6 | export const isFormData = (val) => ( 7 | (typeof FormData !== 'undefined') && 8 | (val instanceof FormData) 9 | ) 10 | 11 | export const runFn = (fn, ...params) => { 12 | if (typeof fn === 'function') return fn(...params) 13 | 14 | return fn 15 | } 16 | 17 | export const isUndefined = val => typeof val === 'undefined' 18 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 统一的日志输出函数,在测试环境时不输出 3 | * @param {string} type 输出类型 log|warn|error 4 | */ 5 | const logByType = (type) => (...out) => { 6 | const env = process.env.NODE_ENV 7 | /* istanbul ignore next */ 8 | if (env === 'test' || env === 'production') return 9 | 10 | /* istanbul ignore next */ 11 | console[type]('[TUA-API]:', ...out) 12 | } 13 | 14 | export const logger = { 15 | log: logByType('log'), 16 | warn: logByType('warn'), 17 | error: logByType('error'), 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/mp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 将小程序 api promise 化 3 | * @param {Function} fn 4 | */ 5 | const promisifyWxApi = (fn) => (args = {}) => ( 6 | new Promise((success, fail) => { 7 | fn({ fail, success, ...args }) 8 | }) 9 | ) 10 | 11 | export { 12 | promisifyWxApi, 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/params.js: -------------------------------------------------------------------------------- 1 | import { 2 | map, 3 | pipe, 4 | join, 5 | merge, 6 | reduce, 7 | } from './fp' 8 | import { logger } from './logger' 9 | import { ERROR_STRINGS } from '../constants' 10 | 11 | /** 12 | * 将对象序列化为 queryString 的形式 13 | * @param {object} data 14 | * @returns {string} 15 | */ 16 | const getParamStrFromObj = (data = {}) => pipe( 17 | Object.keys, 18 | map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`), 19 | join('&'), 20 | )(data) 21 | 22 | /** 23 | * 检查 params 长度和 args 的长度是否匹配,不匹配则打印告警 24 | * @param {object} options 25 | * @param {object} [options.args] 业务侧传递的请求参数 26 | * @param {array|object} options.params 配置中定义的接口数组 27 | * @param {string} [options.apiName] 接口名称 28 | * @return {Boolean} 检查结果(测试使用) 29 | */ 30 | const checkArrayParams = ({ args, params, apiName }) => { 31 | if (!Array.isArray(params)) return true 32 | 33 | if (Object.keys(args).length !== params.length) { 34 | logger.warn(`${apiName}:传递参数长度与 apiConfig 中配置的不同!请检查!`) 35 | return false 36 | } 37 | 38 | return true 39 | } 40 | 41 | /** 42 | * 类似于 vue 的 props,检查传递的参数 43 | * @param {object} options 44 | * @param {object} [options.args] 调用时传递参数 45 | * @param {object} [options.params] 默认参数 46 | * @param {string} [options.apiName] 接口名字 47 | * @param {object} [options.commonParams] 公用默认参数 48 | */ 49 | const getDefaultParamObj = ({ 50 | args = {}, 51 | params = {}, 52 | apiName, 53 | commonParams = {}, 54 | }) => pipe( 55 | Object.keys, 56 | map((key) => { 57 | const val = params[key] 58 | const isRequiredValUndefined = 59 | typeof val === 'object' && 60 | // 兼容 vue 的写法 61 | (val.isRequired || val.required) && 62 | args[key] == null 63 | 64 | if (isRequiredValUndefined) { 65 | logger.error(ERROR_STRINGS.requiredParamFn(apiName, key)) 66 | 67 | /* istanbul ignore next */ 68 | if (process.env.NODE_ENV === 'test') { 69 | throw TypeError(ERROR_STRINGS.requiredParamFn(apiName, key)) 70 | } 71 | } 72 | 73 | const returnVal = typeof val === 'object' ? '' : val 74 | 75 | return { [key]: returnVal } 76 | }), 77 | reduce(merge, commonParams), 78 | )(params) 79 | 80 | /** 81 | * 合并 pathList 下的接口配置和上一级的公共配置 82 | * @param {{ pathList: object[], [k: string]: any }} options 83 | * @return {array} 请求所需参数数组 84 | */ 85 | const apiConfigToReqFnParams = ({ pathList, ...rest }) => 86 | map((pathObj) => ({ ...rest, ...pathObj }))(pathList) 87 | 88 | export { 89 | checkArrayParams, 90 | getDefaultParamObj, 91 | getParamStrFromObj, 92 | apiConfigToReqFnParams, 93 | } 94 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { jest: true }, 3 | globals: { 4 | FormData: true, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /test/__mocks__/wxMock.js: -------------------------------------------------------------------------------- 1 | // mock wx 2 | global.wx = { 3 | request: jest.fn(({ 4 | fail, 5 | success, 6 | complete, 7 | }) => { 8 | setTimeout(() => { 9 | complete && complete() 10 | 11 | const { isTestFail, testData } = wx.__TEST_DATA__ 12 | if (isTestFail) return fail(Error('test')) 13 | 14 | success && success({ data: testData }) 15 | }, 0) 16 | }), 17 | hideLoading: jest.fn(), 18 | showLoading: jest.fn(), 19 | hideNavigationBarLoading: jest.fn(), 20 | showNavigationBarLoading: jest.fn(), 21 | 22 | // 测试数据 23 | __TEST_DATA__: { 24 | isTestFail: false, 25 | testData: null, 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /test/__tests__/axios.test.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import MockAdapter from 'axios-mock-adapter' 3 | 4 | import fakePostConfig from '@examples/apis-web/fake-post' 5 | import { ERROR_STRINGS } from '@/constants' 6 | import { fakeGetApi, fakePostApi } from '@examples/apis-web/' 7 | 8 | const mock = new MockAdapter(axios) 9 | 10 | const params = { param1: 'steve', param2: 'young' } 11 | 12 | const reqAPUrl = 'http://example-base.com/fake-post/array-params' 13 | const reqOPUrl = 'http://example-base.com/fake-post/object-params' 14 | const reqGOPUrl = 'http://example-base.com/fake-get/object-params' 15 | const reqOHUrl = 'http://example-test.com/fake-post/own-baseUrl' 16 | const reqTAUrl = 'http://example-base.com/fake-get/req-type-axios?asyncCp=asyncCp' 17 | const reqEAPUrl = 'http://example-base.com/fake-post/empty-array-params' 18 | const reqMFDUrl = 'http://example-base.com/fake-get/mock-function-data' 19 | const reqBFCUrl = 'http://example-base.com/fake-get/beforeFn-cookie' 20 | const reqCTUrl = 'http://example-base.com/fake-post/custom-transformRequest' 21 | const reqPjUrl = 'http://example-base.com/fake-post/post-json' 22 | const reqRdUrl = 'http://example-base.com/fake-post/raw-data' 23 | 24 | describe('middleware', () => { 25 | test('change baseUrl before request', async () => { 26 | const data = { code: 0, data: 'custom baseUrl' } 27 | const reqHAPUrl = 'http://custom-baseUrl.com/fake-post/array-params' 28 | mock.onPost(reqHAPUrl).reply(200, data) 29 | const resData = await fakePostApi.hap() 30 | 31 | expect(resData).toEqual(data) 32 | }) 33 | }) 34 | 35 | describe('beforeFn cookie', () => { 36 | beforeEach(() => { 37 | // @ts-ignore 38 | mock.resetHistory() 39 | }) 40 | 41 | test('set cookie by beforeFn', async () => { 42 | mock.onGet(reqBFCUrl).reply(200, {}) 43 | await fakeGetApi.beforeFnCookie() 44 | 45 | expect(mock.history.get[0].headers.cookie).toBe('123') 46 | }) 47 | }) 48 | 49 | describe('mock data', () => { 50 | test('mock function data', async () => { 51 | mock.onGet(reqMFDUrl).reply(200, {}) 52 | const resData = await fakeGetApi.mockFnData({ mockCode: 404 }) 53 | 54 | expect(resData.code).toBe(404) 55 | }) 56 | }) 57 | 58 | describe('error handling', () => { 59 | test('non-object params', () => { 60 | // @ts-ignore 61 | return expect(fakePostApi.ap('a')).rejects.toEqual(TypeError(ERROR_STRINGS.argsType)) 62 | }) 63 | 64 | test('error', () => { 65 | mock.onPost(reqEAPUrl).networkError() 66 | 67 | return expect(fakePostApi.eap()).rejects.toEqual(Error('Network Error')) 68 | }) 69 | 70 | test('must pass required params', () => { 71 | // @ts-ignore 72 | return expect(fakePostApi.op()) 73 | .rejects.toEqual(Error(ERROR_STRINGS.requiredParamFn('op', 'param3'))) 74 | }) 75 | }) 76 | 77 | describe('fake get requests', () => { 78 | beforeEach(() => { 79 | // @ts-ignore 80 | mock.resetHistory() 81 | }) 82 | 83 | test('req-type-axios', async () => { 84 | const data = { code: 0, data: 'req-type-axios' } 85 | mock.onGet(reqTAUrl).reply(200, data) 86 | const resData = await fakeGetApi.rta() 87 | 88 | expect(resData).toEqual(data) 89 | }) 90 | 91 | test('runtime get', async () => { 92 | const data = { code: 0, data: 'runtime get' } 93 | mock.onGet(reqAPUrl).reply(200, data) 94 | const resData = await fakePostApi.ap(null, { 95 | type: 'get', 96 | reqType: 'axios', 97 | commonParams: null, 98 | }) 99 | 100 | expect(resData).toEqual(data) 101 | }) 102 | 103 | test('required param', async () => { 104 | const data = [0, 'array data'] 105 | mock.onGet(reqGOPUrl + '?param1=1217¶m2=steve¶m3=young').reply(200, data) 106 | const resData = await fakeGetApi.op({ param3: 'young' }, { reqType: 'axios' }) 107 | 108 | expect(mock.history.get[0].params).toBe(undefined) 109 | expect(resData).toEqual({ code: 0, data: 'array data' }) 110 | }) 111 | }) 112 | 113 | describe('fake post requests', () => { 114 | beforeEach(() => { 115 | // @ts-ignore 116 | mock.resetHistory() 117 | }) 118 | 119 | test('own-baseUrl', async () => { 120 | const data = { code: 0, data: 'own-baseUrl' } 121 | mock.onPost(reqOHUrl).reply(200, data) 122 | const resData = await fakePostApi.oh() 123 | 124 | expect(resData).toEqual(data) 125 | }) 126 | 127 | test('empty-array-params', async () => { 128 | const data = { code: 0, data: 'object data' } 129 | const arrayArgs = [1, 2] 130 | mock.onPost(reqEAPUrl).reply(200, data) 131 | const resData = await fakePostApi.eap(arrayArgs) 132 | 133 | expect(resData).toEqual(data) 134 | expect(mock.history.post[0].data).toEqual(JSON.stringify(arrayArgs)) 135 | }) 136 | 137 | test('array-params', async () => { 138 | const data = { code: 0, data: 'object data' } 139 | mock.onPost(reqAPUrl).reply(200, data) 140 | const resData = await fakePostApi.ap(params) 141 | 142 | expect(resData).toEqual(data) 143 | }) 144 | 145 | test('array-data', async () => { 146 | const data = [0, 'array data'] 147 | mock.onPost(reqOPUrl).reply(200, data) 148 | const resData = await fakePostApi.op({ param3: 'steve' }) 149 | 150 | expect(resData).toEqual({ code: 0, data: 'array data' }) 151 | }) 152 | 153 | test('form-data', async () => { 154 | mock.onPost(reqOHUrl).reply(200, {}) 155 | const formData = new FormData() 156 | formData.append('a', 'a') 157 | formData.append('b', '123') 158 | 159 | await fakePostApi.oh(formData) 160 | 161 | const { 162 | data, 163 | transformRequest, 164 | } = mock.history.post[0] 165 | 166 | expect(data).toBe(formData) 167 | expect(transformRequest).toBe(null) 168 | }) 169 | 170 | test('custom-transformRequest', async () => { 171 | mock.onPost(reqCTUrl).reply(200, {}) 172 | 173 | await fakePostApi.ct() 174 | 175 | const { data } = mock.history.post[0] 176 | 177 | expect(data).toBe('ct') 178 | }) 179 | 180 | test('post-json', async () => { 181 | mock.onPost(reqPjUrl).reply(200, {}) 182 | 183 | await fakePostApi.pj() 184 | 185 | const { data } = mock.history.post[0] 186 | 187 | expect(data).toBe(JSON.stringify(fakePostConfig.commonParams)) 188 | expect(mock.history.post[0].headers['Content-Type']).toBe('application/json;charset=utf-8') 189 | }) 190 | 191 | test('raw-data', async () => { 192 | const data = [0, 'array data'] 193 | mock.onPost(reqRdUrl).reply(200, data) 194 | const resData = await fakePostApi.rd() 195 | 196 | expect(resData).toEqual(data) 197 | }) 198 | }) 199 | -------------------------------------------------------------------------------- /test/__tests__/core.test.js: -------------------------------------------------------------------------------- 1 | import TuaApi from '@/index' 2 | import { ERROR_STRINGS } from '@/constants' 3 | 4 | describe('error handling', () => { 5 | const tuaApi = new TuaApi() 6 | 7 | test('non-function middleware', () => { 8 | // @ts-ignore 9 | expect(() => tuaApi.use('')).toThrow(TypeError(ERROR_STRINGS.middleware)) 10 | }) 11 | 12 | test('unknown reqType', () => { 13 | expect(() => new TuaApi({ reqType: '' })).toThrow(TypeError(ERROR_STRINGS.reqTypeFn(''))) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/__tests__/custom.test.js: -------------------------------------------------------------------------------- 1 | import TuaApi from '@/index' 2 | import { ERROR_STRINGS } from '@/constants' 3 | 4 | const customFetch = jest.fn(() => Promise.resolve({ data: Math.random() })) 5 | 6 | describe('customFetch', () => { 7 | const tuaApi = new TuaApi() 8 | const fooApi = tuaApi.getApi({ 9 | prefix: 'foo', 10 | pathList: [ 11 | { path: 'bar', customFetch }, 12 | { path: 'axios', customFetch, reqType: 'axios' }, 13 | { path: 'customAxios', customFetch, reqType: 'custom' }, 14 | ], 15 | }) 16 | 17 | test('both customFetch and reqType', () => { 18 | expect(() => new TuaApi({ reqType: 'axios', customFetch })).toThrow(TypeError(ERROR_STRINGS.reqTypeAndCustomFetch)) 19 | }) 20 | 21 | test('global customFetch should be called', async () => { 22 | const tuaApi = new TuaApi({ customFetch }) 23 | const fooApi = tuaApi.getApi({ 24 | prefix: 'foo', 25 | pathList: [ 26 | { path: 'globalCustomFetch' }, 27 | ], 28 | }) 29 | await fooApi.globalCustomFetch() 30 | 31 | expect(customFetch).toBeCalled() 32 | }) 33 | 34 | test('local customFetch should be called', async () => { 35 | await fooApi.bar() 36 | 37 | expect(customFetch).toBeCalled() 38 | }) 39 | 40 | test('local customFetch should be called with reqType', async () => { 41 | await fooApi.axios() 42 | 43 | expect(customFetch).toBeCalled() 44 | }) 45 | 46 | test('local customFetch should be called with `custom` reqType', async () => { 47 | await fooApi.customAxios() 48 | 49 | expect(customFetch).toBeCalled() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /test/__tests__/exportUtils.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | getSyncFnMapByApis, 3 | getPreFetchFnKeysBySyncFnMap, 4 | } from '@/exportUtils' 5 | 6 | const noop1 = () => {} 7 | noop1.key = 'noop1' 8 | noop1.params = [] 9 | const noop2 = () => {} 10 | noop2.key = 'noop2' 11 | noop2.params = {} 12 | const noop3 = () => {} 13 | noop3.key = 'noop3' 14 | noop3.params = { a: { required: true } } 15 | const noop4 = () => {} 16 | noop4.key = 'noop4' 17 | noop4.params = {} 18 | 19 | const syncFnMap = getSyncFnMapByApis({ 20 | api1: { path1: noop1, path2: noop2 }, 21 | api2: { path1: noop3, path2: noop4 }, 22 | }) 23 | 24 | test('getSyncFnMapByApis', () => { 25 | expect(syncFnMap).toEqual({ noop1, noop2, noop3, noop4 }) 26 | }) 27 | 28 | test('getPreFetchFnKeysBySyncFnMap', () => { 29 | expect(getPreFetchFnKeysBySyncFnMap(syncFnMap)).toEqual([ 30 | { key: 'noop2' }, 31 | { key: 'noop4' }, 32 | ]) 33 | }) 34 | -------------------------------------------------------------------------------- /test/__tests__/fn.test.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import MockAdapter from 'axios-mock-adapter' 3 | 4 | import fakeFnConfig from '@examples/apis-web/fake-fn' 5 | import { fakeFnApi } from '@examples/apis-web/' 6 | 7 | const mock = new MockAdapter(axios) 8 | 9 | const params = { param1: 'steve', param2: 'young' } 10 | const reqFPUrl = 'http://example-base.com/fake-fn/fn-params' 11 | 12 | describe('function params', () => { 13 | beforeEach(() => { 14 | // @ts-ignore 15 | mock.resetHistory() 16 | }) 17 | 18 | test('should support function type params', async () => { 19 | Math.random = jest.fn(() => 'foo') 20 | mock.onPost(reqFPUrl).reply(200, {}) 21 | await fakeFnApi.fp(params) 22 | 23 | const { data } = mock.history.post[0] 24 | expect(data).toEqual(JSON.stringify({ 25 | ...fakeFnConfig.commonParams(params), 26 | t: 'foo', 27 | p1: params.param1, 28 | p2: params.param2, 29 | })) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/__tests__/fp.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | map, 3 | pipe, 4 | join, 5 | merge, 6 | concat, 7 | filter, 8 | reduce, 9 | compose, 10 | flatten, 11 | mergeAll, 12 | } from '@/utils/' 13 | 14 | describe('functional programming functions', () => { 15 | test('map', () => { 16 | const fn1 = x => x 17 | const fn2 = x => x * 2 18 | const arr = [1, 2, 3] 19 | const obj = { a: 1, b: 2, c: 3 } 20 | 21 | expect(map(fn1)(arr)).toEqual(arr) 22 | expect(map(fn2)(obj)).toEqual({ a: 2, b: 4, c: 6 }) 23 | }) 24 | 25 | test('pipe', () => { 26 | const double = x => x * 2 27 | const square = x => x * x 28 | 29 | expect(pipe()(5)).toBe(5) 30 | expect(pipe(square)(5)).toBe(25) 31 | expect(pipe(square, double)(5)).toBe(50) 32 | expect(pipe(double, square, double)(5)).toBe(200) 33 | }) 34 | 35 | test('join', () => { 36 | expect(join()([])).toBe('') 37 | expect(join()([1, 2])).toBe('1,2') 38 | expect(join('&')([])).toBe('') 39 | expect(join('&')([1, 2])).toBe('1&2') 40 | }) 41 | 42 | test('merge', () => { 43 | const obj = { a: 'a', b: 'b' } 44 | 45 | expect(merge(obj, { c: 'c' })).toEqual({ a: 'a', b: 'b', c: 'c' }) 46 | expect(merge(obj, { b: 'c' })).toEqual({ a: 'a', b: 'c' }) 47 | }) 48 | 49 | test('concat', () => { 50 | expect(concat([])([])).toEqual([]) 51 | expect(concat([])([1, 2])).toEqual([1, 2]) 52 | expect(concat([1, 2])([3, 4])).toEqual([3, 4, 1, 2]) 53 | }) 54 | 55 | test('reduce', () => { 56 | const fn = (x, y) => x + y 57 | const arr = [1, 2, 3] 58 | 59 | expect(reduce(fn)(arr)).toBe(6) 60 | expect(reduce(fn, 10)(arr)).toBe(16) 61 | expect(reduce(fn)([])).toBe(undefined) 62 | }) 63 | 64 | test('filter', () => { 65 | const fn = x => x > 2 66 | const arr = [1, 2, 3] 67 | 68 | expect(filter(fn)(arr)).toEqual([3]) 69 | }) 70 | 71 | test('compose', () => { 72 | const double = x => x * 2 73 | const square = x => x * x 74 | 75 | expect(compose()(5)).toBe(5) 76 | expect(compose(square)(5)).toBe(25) 77 | expect(compose(square, double)(5)).toBe(100) 78 | expect(compose(double, square, double)(5)).toBe(200) 79 | }) 80 | 81 | test('flatten', () => { 82 | const arr1 = [[1], [2], [3]] 83 | const arr2 = [[1], [2], [3, [4]]] 84 | 85 | expect(flatten(arr1)).toEqual([1, 2, 3]) 86 | expect(flatten(arr2)).toEqual([1, 2, 3, 4]) 87 | }) 88 | 89 | test('mergeAll', () => { 90 | const arr = [ 91 | { a: 'a' }, 92 | { b: 'b' }, 93 | { c: 'c' }, 94 | ] 95 | 96 | expect(mergeAll(arr)).toEqual({ 97 | a: 'a', 98 | b: 'b', 99 | c: 'c', 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/__tests__/jsonp.test.js: -------------------------------------------------------------------------------- 1 | import { fakeGetApi } from '@examples/apis-web/' 2 | import fakeGetConfig from '@examples/apis-web/fake-get' 3 | import { ERROR_STRINGS } from '@/constants' 4 | 5 | jest.mock('fetch-jsonp') 6 | 7 | /** 8 | * @type {*} 9 | */ 10 | const fetchJsonp = require('fetch-jsonp') 11 | 12 | const data = [0, 'array data'] 13 | const returnVal = { code: 0, data: 'array data' } 14 | 15 | describe('mock data', () => { 16 | test('mock object data', async () => { 17 | fetchJsonp.mockResolvedValue({ json: () => data }) 18 | /** 19 | * @type {*} 20 | */ 21 | const resData = await fakeGetApi.mockObjectData() 22 | 23 | expect(resData.code).toBe(404) 24 | }) 25 | }) 26 | 27 | describe('fake jsonp requests', () => { 28 | test('jsonp options', async () => { 29 | const url = 'http://example-base.com/fake-get/jsonp-options' 30 | const jsonpOptions = { 31 | ...fakeGetConfig.jsonpOptions, 32 | charset: 'UTF-8', 33 | } 34 | 35 | await fakeGetApi.jsonpOptions() 36 | expect(fetchJsonp).toBeCalledWith(url, jsonpOptions) 37 | 38 | const callback = 'test_cb' 39 | const callbackName = 'test_cbName' 40 | await fakeGetApi.jsonpOptions(null, { callback, callbackName }) 41 | expect(fetchJsonp).toBeCalledWith(url, { 42 | ...jsonpOptions, 43 | jsonpCallback: callback, 44 | jsonpCallbackFunction: callbackName, 45 | }) 46 | }) 47 | 48 | test('async-common-params', async () => { 49 | fetchJsonp.mockResolvedValue({ json: () => data }) 50 | const resData = await fakeGetApi.acp() 51 | 52 | expect(resData).toEqual(returnVal) 53 | }) 54 | 55 | test('array-data', async () => { 56 | fetchJsonp.mockResolvedValue({ json: () => data }) 57 | const resData = await fakeGetApi.ap({}) 58 | 59 | expect(resData).toEqual(returnVal) 60 | }) 61 | 62 | test('object-params', async () => { 63 | fetchJsonp.mockResolvedValue({ json: () => data }) 64 | const resData = await fakeGetApi.op({ param3: 'steve' }) 65 | 66 | expect(resData).toEqual(returnVal) 67 | }) 68 | 69 | test('invalid-req-type', () => { 70 | return expect(fakeGetApi.irt({ param3: 'steve' })) 71 | .rejects.toEqual(TypeError(ERROR_STRINGS.reqTypeFn('foobar'))) 72 | }) 73 | 74 | test('data should be passed through afterFn', async () => { 75 | fetchJsonp.mockResolvedValue({ json: () => data }) 76 | const { afterData } = await fakeGetApi.afterData() 77 | 78 | expect(afterData).toBe('afterData') 79 | }) 80 | 81 | test('there must be some data after afterFn', async () => { 82 | fetchJsonp.mockResolvedValue({ json: () => data }) 83 | const resData = await fakeGetApi.noAfterData() 84 | 85 | expect(resData).toEqual(returnVal) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /test/__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import { ERROR_STRINGS } from '@/constants' 2 | import { 3 | combineUrls, 4 | promisifyWxApi, 5 | checkArrayParams, 6 | getDefaultParamObj, 7 | getParamStrFromObj, 8 | apiConfigToReqFnParams, 9 | } from '@/utils' 10 | 11 | test('combineUrls', () => { 12 | expect(combineUrls(0, 0)).toBe('0/0') 13 | expect(combineUrls(1, 1)).toBe('1/1') 14 | expect(combineUrls(1, null)).toBe('1') 15 | expect(combineUrls(null, 1)).toBe('/1') 16 | expect(combineUrls(undefined, undefined)).toBe('') 17 | expect(combineUrls(undefined, 'users')).toBe('/users') 18 | expect(combineUrls('https://api.github.com', undefined)).toBe('https://api.github.com') 19 | expect(combineUrls('https://api.github.com', 'users')).toBe('https://api.github.com/users') 20 | expect(combineUrls('https://api.github.com', '/users')).toBe('https://api.github.com/users') 21 | expect(combineUrls('https://api.github.com/', '/users')).toBe('https://api.github.com/users') 22 | expect(combineUrls('https://api.github.com/users', '')).toBe('https://api.github.com/users') 23 | expect(combineUrls('https://api.github.com/users', '/')).toBe('https://api.github.com/users/') 24 | }) 25 | 26 | test('promisifyWxApi', () => { 27 | const fn = ({ success }) => setTimeout(() => success('test'), 0) 28 | const promisifiedFn = promisifyWxApi(fn) 29 | 30 | promisifiedFn().then(data => { 31 | expect(data).toBe('test') 32 | }) 33 | }) 34 | 35 | test('checkArrayParams', () => { 36 | expect(checkArrayParams({ params: {} })).toBe(true) 37 | expect(checkArrayParams({ args: { a: 'a' }, params: ['a'] })).toBe(true) 38 | expect(checkArrayParams({ args: {}, params: ['a'] })).toBe(false) 39 | }) 40 | 41 | test('getDefaultParamObj', () => { 42 | expect(getDefaultParamObj({ 43 | commonParams: { a: '1' }, 44 | })).toEqual({ a: '1' }) 45 | 46 | expect(getDefaultParamObj({ 47 | args: { a: '1' }, 48 | params: { b: '2' }, 49 | })).toEqual({ b: '2' }) 50 | 51 | expect(getDefaultParamObj({ 52 | args: { a: '1' }, 53 | params: { b: '2' }, 54 | commonParams: { c: '3' }, 55 | })).toEqual({ b: '2', c: '3' }) 56 | 57 | expect(getDefaultParamObj({ 58 | params: { a: { required: false } }, 59 | })).toEqual({ a: '' }) 60 | 61 | expect(() => getDefaultParamObj({ 62 | params: { b: { required: true } }, 63 | apiName: 'steve', 64 | })).toThrow(Error(ERROR_STRINGS.requiredParamFn('steve', 'b'))) 65 | 66 | expect(() => getDefaultParamObj({ 67 | params: { c: { isRequired: true } }, 68 | apiName: 'steve', 69 | })).toThrow(Error(ERROR_STRINGS.requiredParamFn('steve', 'c'))) 70 | }) 71 | 72 | test('getParamStrFromObj', () => { 73 | expect(getParamStrFromObj()).toBe('') 74 | expect(getParamStrFromObj({})).toBe('') 75 | expect(getParamStrFromObj({ a: 1, b: 2 })).toBe('a=1&b=2') 76 | expect(getParamStrFromObj({ a: 1, b: 2, c: '哈喽' })).toBe('a=1&b=2&c=%E5%93%88%E5%96%BD') 77 | expect(getParamStrFromObj({ 哈喽: '哈喽' })).toBe('%E5%93%88%E5%96%BD=%E5%93%88%E5%96%BD') 78 | }) 79 | 80 | test('apiConfigToReqFnParams', () => { 81 | expect(apiConfigToReqFnParams({ 82 | pathList: [ 83 | { path: 'api1', a: 'aa' }, 84 | { path: 'api2', b: 'bb' }, 85 | ], 86 | a: 'a', 87 | b: 'b', 88 | })).toEqual([ 89 | { path: 'api1', a: 'aa', b: 'b' }, 90 | { path: 'api2', a: 'a', b: 'bb' }, 91 | ]) 92 | }) 93 | -------------------------------------------------------------------------------- /test/__tests__/wx.test.js: -------------------------------------------------------------------------------- 1 | import '../__mocks__/wxMock' 2 | import TuaApi from '@/index' 3 | import { ERROR_STRINGS } from '@/constants' 4 | 5 | import fakeWx from '@examples/apis-mp/fake-wx' 6 | import { mockApi, fakeWxApi } from '@examples/apis-mp/' 7 | 8 | const testObjData = { code: 0, data: 'object data' } 9 | const testArrData = [0, 'array data'] 10 | 11 | describe('mock data', () => { 12 | beforeEach(() => { 13 | wx.__TEST_DATA__ = {} 14 | }) 15 | 16 | test('common mock data', async () => { 17 | wx.__TEST_DATA__ = { testData: testObjData } 18 | const resData = await mockApi.bar({ __mockData__: { code: 404, data: {} } }) 19 | 20 | expect(resData.code).toBe(404) 21 | }) 22 | 23 | test('self mock data', async () => { 24 | wx.__TEST_DATA__ = { testData: testObjData } 25 | const resData = await mockApi.foo({ __mockData__: { code: 404, data: {} } }) 26 | 27 | expect(resData.code).toBe(500) 28 | }) 29 | 30 | test('null mock data', async () => { 31 | wx.__TEST_DATA__ = { testData: testObjData } 32 | const resData = await mockApi.null({ __mockData__: { code: 404, data: {} } }) 33 | 34 | expect(resData).toEqual({ code: 0, data: 'object data' }) 35 | }) 36 | 37 | test('dynamic object mock data', async () => { 38 | wx.__TEST_DATA__ = { testData: testObjData } 39 | mockApi.null.mock = { code: 123 } 40 | const resData = await mockApi.null() 41 | 42 | expect(resData.code).toBe(123) 43 | }) 44 | 45 | test('dynamic function mock data', async () => { 46 | wx.__TEST_DATA__ = { testData: testObjData } 47 | mockApi.foo.mock = ({ mockCode }) => ({ code: mockCode }) 48 | const resData = await mockApi.foo({ mockCode: 123 }) 49 | 50 | expect(resData.code).toBe(123) 51 | }) 52 | }) 53 | 54 | describe('middleware', () => { 55 | const tuaApi = new TuaApi() 56 | const fakeWxApi = tuaApi.getApi(fakeWx) 57 | const globalMiddlewareFn = jest.fn(async (ctx, next) => { 58 | expect(ctx.req.host).toBeDefined() 59 | expect(ctx.req.baseUrl).toBeDefined() 60 | expect(ctx.req.type).toBeDefined() 61 | expect(ctx.req.method).toBeDefined() 62 | expect(ctx.req.path).toBeDefined() 63 | expect(ctx.req.prefix).toBeDefined() 64 | expect(ctx.req.reqType).toBeDefined() 65 | expect(ctx.req.reqParams).toBeDefined() 66 | expect(ctx.req.axiosOptions).toBeDefined() 67 | expect(ctx.req.jsonpOptions).toBeDefined() 68 | expect(ctx.req.reqFnParams).toBeDefined() 69 | 70 | expect(ctx.req.callbackName).toBeUndefined() 71 | 72 | await next() 73 | 74 | expect(ctx.reqTime).toBeDefined() 75 | expect(ctx.startTime).toBeDefined() 76 | expect(ctx.endTime).toBeDefined() 77 | 78 | expect(ctx.res.data).toBeDefined() 79 | expect(ctx.res.rawData).toBeDefined() 80 | }) 81 | 82 | tuaApi.use(globalMiddlewareFn) 83 | 84 | beforeEach(() => { 85 | wx.__TEST_DATA__ = { testData: {} } 86 | }) 87 | 88 | test('useGlobalMiddleware', async () => { 89 | await fakeWxApi.arrayData() 90 | expect(globalMiddlewareFn).toBeCalledTimes(0) 91 | await fakeWxApi.fail() 92 | expect(globalMiddlewareFn).toBeCalledTimes(1) 93 | }) 94 | }) 95 | 96 | describe('fake wx requests', () => { 97 | beforeEach(() => { 98 | wx.__TEST_DATA__ = {} 99 | }) 100 | 101 | test('same key', () => { 102 | expect(fakeWxApi.fail.key).not.toEqual(fakeWxApi.anotherFail.key) 103 | }) 104 | 105 | test('object-data', async () => { 106 | wx.__TEST_DATA__ = { testData: testObjData } 107 | const resData = await fakeWxApi.objectData({ param3: '123' }) 108 | 109 | expect(resData).toEqual({ code: 0, data: 'object data' }) 110 | }) 111 | 112 | test('array-data', async () => { 113 | wx.__TEST_DATA__ = { testData: testArrData } 114 | const resData = await fakeWxApi.arrayData(null) 115 | 116 | expect(resData).toEqual({ code: 0, data: 'array data' }) 117 | }) 118 | 119 | test('fail', () => { 120 | wx.__TEST_DATA__ = { isTestFail: true } 121 | 122 | return expect(fakeWxApi.fail({ a: 'b' })) 123 | .rejects.toEqual(Error('test')) 124 | }) 125 | 126 | test('no-beforeFn', () => { 127 | return expect(fakeWxApi.noBeforeFn()) 128 | .rejects.toEqual(Error(ERROR_STRINGS.noData)) 129 | }) 130 | 131 | test('hide-loading', async () => { 132 | wx.showLoading.mockClear() 133 | wx.__TEST_DATA__ = { testData: testObjData } 134 | const resData = await fakeWxApi.hideLoading() 135 | 136 | expect(resData).toEqual({ code: 0, data: 'object data' }) 137 | expect(wx.showLoading).toHaveBeenCalledTimes(0) 138 | }) 139 | 140 | test('type-get', async () => { 141 | wx.showLoading.mockClear() 142 | wx.__TEST_DATA__ = { testData: testObjData } 143 | await fakeWxApi.typeGet() 144 | const [[{ method }]] = wx.request.mock.calls 145 | 146 | expect(method).toBe('GET') 147 | expect(wx.showLoading).toHaveBeenCalledTimes(1) 148 | }) 149 | 150 | test('unknown-type', () => { 151 | return expect(fakeWxApi.unknownType()) 152 | .rejects.toEqual(Error(ERROR_STRINGS.unknownMethodFn('FOO'))) 153 | }) 154 | 155 | test('nav-loading', async () => { 156 | wx.showNavigationBarLoading.mockClear() 157 | wx.__TEST_DATA__ = { testData: testObjData } 158 | const resData = await fakeWxApi.navLoading() 159 | 160 | expect(resData).toEqual({ code: 0, data: 'object data' }) 161 | expect(wx.showNavigationBarLoading).toHaveBeenCalledTimes(1) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "baseUrl": ".", 6 | "target": "es5", 7 | "resolveJsonModule": true, 8 | "experimentalDecorators": true, 9 | "lib": [ 10 | "dom", 11 | "es2015" 12 | ], 13 | "paths":{ 14 | "@/*": ["src/*"], 15 | "@examples/*": ["examples/*"] 16 | } 17 | }, 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | --------------------------------------------------------------------------------