From 52a394409b543df1f145af66842d09085e42fe2d Mon Sep 17 00:00:00 2001 From: fxy060608 Date: Fri, 12 Jul 2019 17:09:38 +0800 Subject: [PATCH] feat(h5): add interceptor --- src/core/helpers/api.js | 3 + src/core/helpers/interceptor.js | 201 ++++++++++++++++++ src/core/helpers/promise.js | 18 +- src/core/service/api/interceptor.js | 13 ++ .../unit/core/service/api/interceptor.spec.js | 137 ++++++++++++ .../{events.spec.js => events.spec.bak.js} | 0 6 files changed, 366 insertions(+), 6 deletions(-) create mode 100644 src/core/helpers/interceptor.js create mode 100644 src/core/service/api/interceptor.js create mode 100644 tests/unit/core/service/api/interceptor.spec.js rename tests/unit/core/view/components/{events.spec.js => events.spec.bak.js} (100%) diff --git a/src/core/helpers/api.js b/src/core/helpers/api.js index 406f8c98c..bad489ed2 100644 --- a/src/core/helpers/api.js +++ b/src/core/helpers/api.js @@ -240,6 +240,9 @@ export function wrapperUnimplemented (name) { } export function wrapper (name, invokeMethod, extras) { + if (!isFn(invokeMethod)) { + return invokeMethod + } return function (...args) { if (isSyncApi(name)) { if (validateParams(name, args, -1)) { diff --git a/src/core/helpers/interceptor.js b/src/core/helpers/interceptor.js new file mode 100644 index 000000000..577105eea --- /dev/null +++ b/src/core/helpers/interceptor.js @@ -0,0 +1,201 @@ +import { + isFn, + isPlainObject +} from 'uni-shared' + +import { + shouldPromise +} from './promise' + +const HOOKS = [ + 'invoke', + 'success', + 'fail', + 'complete', + 'returnValue' +] + +const globalInterceptors = {} +const scopedInterceptors = {} + +function mergeHook (parentVal, childVal) { + const res = childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal : [childVal] + : parentVal + return res + ? dedupeHooks(res) + : res +} + +function dedupeHooks (hooks) { + const res = [] + for (let i = 0; i < hooks.length; i++) { + if (res.indexOf(hooks[i]) === -1) { + res.push(hooks[i]) + } + } + return res +} + +function removeHook (hooks, hook) { + const index = hooks.indexOf(hook) + if (index !== -1) { + hooks.splice(index, 1) + } +} + +function mergeInterceptorHook (interceptor, option) { + Object.keys(option).forEach(hook => { + if (HOOKS.indexOf(hook) !== -1 && isFn(option[hook])) { + interceptor[hook] = mergeHook(interceptor[hook], option[hook]) + } + }) +} + +function removeInterceptorHook (interceptor, option) { + if (!interceptor || !option) { + return + } + Object.keys(option).forEach(hook => { + if (HOOKS.indexOf(hook) !== -1 && isFn(option[hook])) { + removeHook(interceptor[hook], option[hook]) + } + }) +} + +export function addInterceptor (method, option) { + if (typeof method === 'string' && isPlainObject(option)) { + if (!shouldPromise(method)) { + return console.warn(`${method} 不支持设置拦截器`) + } + mergeInterceptorHook(scopedInterceptors[method] || (scopedInterceptors[method] = {}), option) + } else if (isPlainObject(method)) { + mergeInterceptorHook(globalInterceptors, method) + } +} + +export function removeInterceptor (method, option) { + if (typeof method === 'string') { + if (isPlainObject(option)) { + removeInterceptorHook(scopedInterceptors[method], option) + } else { + delete scopedInterceptors[method] + } + } else if (isPlainObject(method)) { + removeInterceptorHook(globalInterceptors, method) + } +} + +function wrapperHook (hook) { + return function (data) { + return hook(data) || data + } +} + +function isPromise (obj) { + return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' +} + +function queue (hooks, data) { + let promise = false + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i] + if (promise) { + promise = Promise.then(wrapperHook(hook)) + } else { + const res = hook(data) + if (isPromise(res)) { + promise = Promise.resolve(res) + } + if (res === false) { + return { + then () {} + } + } + } + } + return promise || { + then (callback) { + return callback(data) + } + } +} + +function wrapperOptions (interceptor, options = {}) { + ['success', 'fail', 'complete'].forEach(name => { + if (Array.isArray(interceptor[name])) { + const oldCallback = options[name] + options[name] = function callbackInterceptor (res) { + queue(interceptor[name], res).then((res) => { + /* eslint-disable no-mixed-operators */ + return isFn(oldCallback) && oldCallback(res) || res + }) + } + } + }) + return options +} + +export function wrapperReturnValue (method, returnValue) { + const returnValueHooks = [] + if (Array.isArray(globalInterceptors.returnValue)) { + returnValueHooks.push(...globalInterceptors.returnValue) + } + const interceptor = scopedInterceptors[method] + if (interceptor && Array.isArray(interceptor.returnValue)) { + returnValueHooks.push(...interceptor.returnValue) + } + returnValueHooks.forEach(hook => { + returnValue = hook(returnValue) || returnValue + }) + return returnValue +} + +function getApiInterceptorHooks (method) { + const interceptor = Object.create(null) + Object.keys(globalInterceptors).forEach(hook => { + if (hook !== 'returnValue') { + interceptor[hook] = globalInterceptors[hook].slice() + } + }) + const scopedInterceptor = scopedInterceptors[method] + if (scopedInterceptor) { + Object.keys(scopedInterceptor).forEach(hook => { + if (hook !== 'returnValue') { + interceptor[hook] = (interceptor[hook] || []).concat(scopedInterceptor[hook]) + } + }) + } + return interceptor +} + +export function invokeApi (method, api, options, ...params) { + const interceptor = getApiInterceptorHooks(method) + if (interceptor) { + if (Array.isArray(interceptor.invoke)) { + const res = queue(interceptor.invoke, options) + return res.then((options) => { + return api(wrapperOptions(interceptor, options), ...params) + }) + } else { + return api(wrapperOptions(interceptor, options), ...params) + } + } + return api(options, ...params) +} + +export const promiseInterceptor = { + returnValue (res) { + if (!isPromise(res)) { + return res + } + return res.then(res => { + return res[1] + }).catch(res => { + return res[0] + }) + } +} diff --git a/src/core/helpers/promise.js b/src/core/helpers/promise.js index f356cb0d7..3d035dba0 100644 --- a/src/core/helpers/promise.js +++ b/src/core/helpers/promise.js @@ -2,7 +2,13 @@ import { isFn } from 'uni-shared' -const SYNC_API_RE = /^\$|getSubNVueById|requireNativePlugin|upx2px|hideKeyboard|canIUse|^create|Sync$|Manager$|base64ToArrayBuffer|arrayBufferToBase64/ +import { + invokeApi, + wrapperReturnValue +} from './interceptor' + +const SYNC_API_RE = + /^\$|interceptors|Interceptor$|getSubNVueById|requireNativePlugin|upx2px|hideKeyboard|canIUse|^create|Sync$|Manager$|base64ToArrayBuffer|arrayBufferToBase64/ const CONTEXT_API_RE = /^create|Manager$/ @@ -49,10 +55,10 @@ export function promisify (name, api) { } return function promiseApi (options = {}, ...params) { if (isFn(options.success) || isFn(options.fail) || isFn(options.complete)) { - return api(options, ...params) + return wrapperReturnValue(name, invokeApi(name, api, options, ...params)) } - return handlePromise(new Promise((resolve, reject) => { - api(Object.assign({}, options, { + return wrapperReturnValue(name, handlePromise(new Promise((resolve, reject) => { + invokeApi(name, api, Object.assign({}, options, { success: resolve, fail: reject }), ...params) @@ -68,6 +74,6 @@ export function promisify (name, api) { ) } } - })) + }))) } -} +} diff --git a/src/core/service/api/interceptor.js b/src/core/service/api/interceptor.js new file mode 100644 index 000000000..f1bb52b87 --- /dev/null +++ b/src/core/service/api/interceptor.js @@ -0,0 +1,13 @@ +import { + promiseInterceptor +} from 'uni-helpers/interceptor' + +export { + addInterceptor, + removeInterceptor +} + from 'uni-helpers/interceptor' + +export const interceptors = { + promiseInterceptor +} diff --git a/tests/unit/core/service/api/interceptor.spec.js b/tests/unit/core/service/api/interceptor.spec.js new file mode 100644 index 000000000..6f99ff6bb --- /dev/null +++ b/tests/unit/core/service/api/interceptor.spec.js @@ -0,0 +1,137 @@ +import { + expect +} from 'chai' + +describe('接口`addInterceptor`', () => { + it('同步拦截器 invoke', done => { + const setStorageInterceptor = { + invoke(args) { + args.data = 2 + } + } + uni.addInterceptor('setStorage', setStorageInterceptor) + + uni.setStorage({ + key: 'test', + data: 1 + }).then(function() { + expect(uni.getStorageSync('test')).eq(2) + uni.removeInterceptor('setStorage') + return uni.setStorage({ + key: 'test', + data: 1 + }) + }).then(function() { + expect(uni.getStorageSync('test')).eq(1) + done() + }) + }) + it('同步拦截器 callback', done => { + const setStorageInterceptor = { + success(res) { + res.data = 2 + }, + complete(res) { + res.data = 3 + } + } + uni.addInterceptor('setStorage', setStorageInterceptor) + uni.setStorage({ + key: 'test', + data: 1, + success(res) { + expect(res.data).eq(2) + }, + complete(res) { + uni.removeInterceptor('setStorage') + expect(res.data).eq(3) + done() + } + }) + }) + it('异步拦截器 invoke', done => { + const setStorageInterceptor = { + invoke(args) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + args.data = 2 + resolve(args) + }, 200) + }) + } + } + uni.addInterceptor('setStorage', setStorageInterceptor) + + uni.setStorage({ + key: 'test', + data: 1 + }).then(function() { + expect(uni.getStorageSync('test')).eq(2) + uni.removeInterceptor('setStorage') + return uni.setStorage({ + key: 'test', + data: 1 + }) + }).then(function() { + expect(uni.getStorageSync('test')).eq(1) + done() + }) + }) + it('异步拦截器 callback', done => { + const setStorageInterceptor = { + success(res) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + res.data = 2 + resolve(res) + }, 200) + }) + }, + complete(res) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + res.data = 3 + resolve(res) + }, 1000) + }) + } + } + uni.addInterceptor('setStorage', setStorageInterceptor) + uni.setStorage({ + key: 'test', + data: 1, + success(res) { + expect(res.data).eq(2) + }, + complete(res) { + expect(res.data).eq(3) + uni.setStorage({ + key: 'test', + data: 1 + }).then(function(res) { + uni.removeInterceptor('setStorage') + expect(res[1].data).eq(2) + done() + }) + } + }) + + }) + it('全局拦截器 promiseInterceptor', done => { + uni.addInterceptor(uni.interceptors.promiseInterceptor) + uni.setStorageSync('test', 1) + uni.getStorage({ + key: 'test' + }).then(res => { + expect(res.data).eq(1) + uni.removeInterceptor(uni.interceptors.promiseInterceptor) + uni.getStorage({ + key: 'test' + }).then(res => { + expect(res[1].data).eq(1) + done() + }) + }) + + }) +}) diff --git a/tests/unit/core/view/components/events.spec.js b/tests/unit/core/view/components/events.spec.bak.js similarity index 100% rename from tests/unit/core/view/components/events.spec.js rename to tests/unit/core/view/components/events.spec.bak.js -- GitLab