Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fetch): add response method. #947

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a3f967f
Merge pull request #1 from didi/master
nianxiongdi Jan 24, 2022
18e646b
Merge pull request #2 from didi/master
nianxiongdi Jan 28, 2022
680f8d7
feat(proxy): add proxy response method.
Feb 7, 2022
8e26843
Merge pull request #3 from didi/master
nianxiongdi Feb 7, 2022
38e5f5f
fix(*): add response params.
Feb 7, 2022
6e290b8
docs(*): add proxy desc.
Feb 7, 2022
9bd2db9
Merge branch 'master' of https://github.com/nianxiongdi/mpx into feat…
Feb 7, 2022
7451dbe
Merge pull request #4 from didi/master
nianxiongdi Feb 11, 2022
38b507d
Merge branch 'master' of https://github.com/nianxiongdi/mpx into feat…
Feb 11, 2022
7042de9
fix(*): fix eslint.
Feb 11, 2022
4c612b0
Merge branch 'master' of https://github.com/didi/mpx into dev-qy-mock
Feb 14, 2022
b46c842
fix(*): mock api.
Feb 14, 2022
9eaae66
fix(mock): add mock file.
Feb 15, 2022
f404d9f
fix(*): proxy position.
Feb 16, 2022
504a819
fix(*): fix code.
Feb 16, 2022
74ef19f
docs(*): add mock docs.
Feb 16, 2022
d4bbe94
fix(*): fix eslint.
Feb 17, 2022
f5f136b
fix(*): merge code.
nianxiongdi Mar 16, 2022
c622138
Merge branch 'master' into dev-qy-mock
hiyuki Mar 22, 2022
a789a4b
fix(*): add mock response handle.
nianxiongdi Apr 7, 2022
318a08e
Merge branch 'master' of https://github.com/didi/mpx into dev-qy-mock
nianxiongdi Apr 7, 2022
633e0c0
Merge branch 'dev-qy-mock' of https://github.com/didi/mpx into dev-qy…
nianxiongdi Apr 7, 2022
d3ce042
fix(*): add url params judge.
nianxiongdi Apr 8, 2022
7fcaf28
修改validate方法和doTest方法
Blackgan3 Apr 15, 2022
3f3c14a
Merge branch 'master' into dev-qy-mock
Blackgan3 Apr 18, 2022
6e3f24c
增加mock相关ts定义
Blackgan3 Apr 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs-vuepress/api/extend.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ mpx.xfetch.interceptors.response.use(function(res) {
如果设置了此项,匹配结果以此项为准,以上规则均不再生效。
:::


- **proxy**

类型:`object`
Expand Down Expand Up @@ -394,6 +395,80 @@ console.log(mpx.xfetch.getProxy())
mpx.xfetch.clearProxy()
```


### mock 数据模拟

> 为用户提供本地mock功能,可用于开发阶段或单测等场景。

##### setMock
> 配置模拟数据,可以传入一个数组或者一个对象,按照顺序依次匹配

- **参数:**

类型: `{Array | Object}`
- **test**

与setProxy的test属性保持一致

- **mock**

类型: `{Function}`

表示自定义的mock函数,其返回值为接口mock数据。this代表fetch的第一个参数。

- **示例:**
```js
mpx.xfetch.setMock([{
test: {
custom(origin) {
return ~origin.url.indexOf('/api/getlist')
}
},
mock: () => {
return {
code: 200,
msg: 'succes',
data: [{
id: '1',
name: 'qy'
}, {
id: '2',
name: 'wl'
}]
}
}
}, {
test: {
header: {
'content-type': 'application/x-www-form-urlencoded'
},
method: 'GET',
params: {
a: 1
},
protocol: 'http:',
host: '10.11.11.123',
port: '8000',
path: '/api/user'
},
mock: () => {
return {
code: 200,
msg: 'succes',
data: {
name: 'qy'
}
}
}
}])
```

#### getMock
> 查看已有的mock配置

#### clearMock
> 清除所有的mock配置

## api-proxy
Mpx目前已经支持的API转换列表,供参考

Expand Down
36 changes: 36 additions & 0 deletions packages/fetch/@types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,40 @@ interface CreateOption {
ratio?: number
}

interface TestOption {
url?: string
protocal?: string
host?: string
port?: string
path?: string
params?: Record<string, any>
data?: Record<string, any>
header?: Record<string, any>
method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT'
custom?: (...args: any[]) => boolean
}

interface ProxyResultOption extends TestOption {
custom?: (...args: any[]) => any
}

interface ProxyOption {
test: TestOption
proxy: ProxyResultOption
waterfall: boolean
}

interface MockOption {
test: TestOption
mock: (...args: any[]) => any
}

// @ts-ignore
type fetchT = <T>(option: fetchOption, priority?: 'normal' | 'low') => Promise<WechatMiniprogram.RequestSuccessCallbackResult<T> & { requestConfig: fetchOption }>
type addLowPriorityWhiteListT = (rules: string | RegExp | Array<string | RegExp>) => void
type createT = (option?: CreateOption) => xfetch
type setProxyT = (option: ProxyOption | Array<ProxyOption>) => void
type setMockT = (option: MockOption | Array<MockOption>) => void

export interface InterceptorsRR {
use: (fulfilled: (...args: any[]) => any, rejected?: (...args: any[]) => any) => (...args: any[]) => any
Expand All @@ -38,6 +68,12 @@ export interface xfetch {
CancelToken: CancelTokenClass
create: createT
interceptors: Interceptors
setProxy: setProxyT
getProxy: () => ProxyOption | Array<ProxyOption>
clearProxy: () => void
setMock: setMockT
getMock: () => MockOption | Array<MockOption>
clearMock: () => void
}

declare module '@mpxjs/core' {
Expand Down
25 changes: 25 additions & 0 deletions packages/fetch/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"presets": [
[
"@babel/env",
{
"modules": false,
"shippedProposals": true
}
]
],
"sourceType": "unambiguous",
"env": {
"test": {
"presets": [
[
"@babel/env",
{
"shippedProposals": true
}
]
]
}
}
}

7 changes: 7 additions & 0 deletions packages/fetch/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"transform": {
"^.+\\.(js|jsx)?$": "babel-jest"
},
"testEnvironment": "jsdom"
}

16 changes: 16 additions & 0 deletions packages/fetch/src/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import { isFunction, doTest, transformRes, buildResponse } from './util'

// mock
export function requestMock (options = [], config) {
const configBackup = Object.assign({}, config) // 备份请求配置
for (let item of options) {
const { test, mock: callback } = item
// custom不存在时,进行url参数匹配
const matched = doTest(configBackup, test).matched
if (isFunction(callback) && matched) {
let data = Object.assign({ requestConfig: config }, transformRes(buildResponse(callback(config))))
return Promise.resolve(data)
}
}
}
123 changes: 2 additions & 121 deletions packages/fetch/src/proxy.js
Original file line number Diff line number Diff line change
@@ -1,123 +1,4 @@
import { match } from 'path-to-regexp'
import { isArray, isFunction, isNotEmptyArray, isNotEmptyObject, isString, parseUrl, deepMerge } from './util'

/**
* 匹配项所有属性值,在源对象都能找到匹配
* @param test 匹配项
* @param input 源对象
* @returns {boolean}
*/
function attrMatch (test = {}, input = {}) {
let result = true
for (const key in test) {
// value 值为 true 时 key 存在即命中匹配
if (test.hasOwnProperty(key) && input.hasOwnProperty(key)) {
if (test[key] === true) continue
// value 如果不是字符串需要进行序列化之后再匹配
const testValue = isString(test[key]) ? test[key] : JSON.stringify(test[key])
const inputValue = isString(input[key]) ? input[key] : JSON.stringify(input[key])
if (testValue !== inputValue) {
result = false
}
} else {
result = false
}
}
return result
}

/**
* 匹配 rule 中的对应项
* @param config 原请求配置项
* @param test 匹配配置
* @returns {{matchParams, matched: boolean}}
*/
function doTest (config, test) {
const { url, params = {}, data = {}, header = {}, method = 'GET' } = config
const {
url: tUrl = '',
protocol: tProtocol = '',
host: tHost = '',
port: tPort = '',
path: tPath = '',
search: tSearch = '',
params: tParams = {},
data: tData = {},
header: tHeader = {},
method: tMethod = ''
} = test

const { baseUrl, protocol, hostname, port, path, search } = parseUrl(url)

// 如果待匹配项为空,则认为匹配成功
// url 匹配
let urlMatched = false
let matchParams = {}
if (tUrl) {
// 处理协议头
const protocolReg = /^(?:\w+(\\)?:|(:\w+))\/\//

const hasProtocol = protocolReg.exec(tUrl)

let handledTUrl = tUrl

if (hasProtocol) {
if (!hasProtocol[1] && !hasProtocol[2]) {
handledTUrl = tUrl.replace(':', '\\:')
}
} else {
handledTUrl = (tUrl.startsWith('//') ? ':protocol' : ':protocol//') + tUrl
}

try {
// 匹配结果参数
const matcher = match(handledTUrl)
const result = matcher(baseUrl)
urlMatched = !!result
matchParams = result.params
} catch (error) {
console.error('Test url 不符合规范,test url 中存在 : 或者 ? 等保留字符,请在前面添加 \\ 进行转义,如 https\\://baidu.com/xxx.')
}
} else {
// protocol 匹配
const protocolMatched = tProtocol ? tProtocol === protocol : true
// host 匹配
const hostMatched = tHost ? tHost === hostname : true
// port 匹配
const portMatched = tPort ? tPort === port : true
// path 匹配
const pathMatched = tPath ? tPath === path : true

urlMatched = protocolMatched && hostMatched && portMatched && pathMatched
}
// search 匹配
const searchMatched = tSearch ? search.includes(tSearch) : true
// params 匹配
const paramsMatched = isNotEmptyObject(tParams) ? attrMatch(tParams, params) : true
// data 匹配
const likeGet = /^GET|DELETE|HEAD$/i.test(method)
const dataMatched = isNotEmptyObject(tData) ? attrMatch(tData, likeGet ? params : data) : true
// header 匹配
const headerMatched = isNotEmptyObject(tHeader) ? attrMatch(tHeader, header) : true
// method 匹配
let methodMatched = false
if (isArray(tMethod)) {
const tMethodUpper = tMethod.map((item) => {
return item.toUpperCase()
})
methodMatched = isNotEmptyArray(tMethodUpper) ? tMethodUpper.indexOf(method) > -1 : true
} else if (isString(tMethod)) {
methodMatched = tMethod ? tMethod.toUpperCase() === method : true
}

// 是否匹配
const matched = urlMatched && searchMatched && paramsMatched && dataMatched && headerMatched && methodMatched

return {
matched,
matchParams
}
}
import { isFunction, isNotEmptyObject, parseUrl, deepMerge, doTest } from './util'

/**
* 处理 config
Expand Down Expand Up @@ -202,7 +83,7 @@ export function requestProxy (options, config) {
options && options.some((item) => {
const { test, proxy, waterfall } = item
const { matched, matchParams } = doTest(configBackup, test)
if ((isFunction(test.custom) && test.custom(configBackup)) || matched) {
if (matched) {
// 匹配时
newConfig = doProxy(newConfig, proxy, matchParams)

Expand Down
32 changes: 30 additions & 2 deletions packages/fetch/src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export function isThenable (obj) {
return obj && typeof obj.then === 'function'
}

// 排除一些特定属性,是否为空对象
export function isEmptyObjectAttr (obj, excludeAttrs = []) {
return !(obj && isObject(obj) && Object.keys(obj).some(key => {
if (~excludeAttrs.indexOf(key)) return false
return !!obj[key]
}))
}

// 不为空对象
export function isNotEmptyObject (obj) {
return obj && isObject(obj) && Object.keys(obj).length > 0
Expand Down Expand Up @@ -252,7 +260,8 @@ export function doTest (config, test) {
params: tParams = {},
data: tData = {},
header: tHeader = {},
method: tMethod = ''
method: tMethod = '',
custom: tCustom = ''
} = test

const { baseUrl, protocol, hostname, port, path, search } = parseUrl(url)
Expand Down Expand Up @@ -299,6 +308,12 @@ export function doTest (config, test) {
urlMatched = protocolMatched && hostMatched && portMatched && pathMatched
}

// 判断custom以外的属性值为空
const urlParamsExist = !isEmptyObjectAttr(test, ['custom'])

// 自定义匹配函数
let customMatched = isFunction(tCustom) && tCustom(config)

// search 匹配
const searchMatched = tSearch ? search.includes(tSearch) : true
// params 匹配
Expand All @@ -320,7 +335,7 @@ export function doTest (config, test) {
}

// 是否匹配
const matched = urlMatched && searchMatched && paramsMatched && dataMatched && headerMatched && methodMatched
let matched = customMatched || (urlParamsExist && urlMatched && searchMatched && paramsMatched && dataMatched && headerMatched && methodMatched)

return {
matched,
Expand All @@ -347,3 +362,16 @@ export function checkCacheConfig (thisConfig, catchData) {
JSON.stringify(sortObject(thisConfig.params)) === JSON.stringify(catchData.params) &&
thisConfig.method === catchData.method
}

export function buildResponse (data) {
let response = {
header: {
'Content-Type': 'text/plain; charset=utf-8'
},
data: {},
cookies: []
}

if (!data.hasOwnProperty('statusCode') && !data.hasOwnProperty('status')) return Object.assign(response, { data: data, statusCode: 200 })
return data
}
Loading