Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions packages/playground/assets/src/pages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"pages": [
{
"path": "pages/index/index"
},
{
"path": "pages/interceptor/interceptor"
}
],
"subPackages": [
Expand Down
1 change: 1 addition & 0 deletions packages/playground/assets/src/pages/index/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<scroll-view>
<navigator url="/pages/sub/test/test">test</navigator>
<navigator url="/pages/interceptor/interceptor">拦截器示例</navigator>
<test-com></test-com>
<view>
<view>3:{{ invalidStatic }}</view>
Expand Down
237 changes: 237 additions & 0 deletions packages/playground/assets/src/pages/interceptor/interceptor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<script setup>
import { delay } from "es-toolkit";
import { ref } from "vue";

const navigateActive = ref(false);

// 添加日志
function addLog(msg) {
const time = new Date().toLocaleTimeString();
console.log(`[${time}] ${msg}`);
}
// 添加 navigateTo 拦截器
function addNavigateInterceptor() {
if (navigateActive.value) {
addLog("navigateTo 拦截器已存在,无需重复添加");
return;
}
uni.addInterceptor("navigateTo", {
invoke: (args) => {
addLog(`🚀 [navigateTo-invoke] 目标页面:${args.url || "未知"}`);
return false;
},
success: () => {
addLog("🚀 [navigateTo-success] 跳转成功");
},
fail: (err) => {
addLog(`🚀 [navigateTo-fail] 跳转失败:${err.errMsg || "未知错误"}`);
},
complete: () => {
addLog("🚀 [navigateTo-complete] 跳转操作完成");
},
});
navigateActive.value = true;
addLog("✅ navigateTo 拦截器已添加");
}
// 添加异步 navigateTo 拦截器
function addAsyncNavigateInterceptor() {
if (navigateActive.value) {
addLog("navigateTo 拦截器已存在,无需重复添加");
return;
}
uni.addInterceptor("navigateTo", {
invoke: async (args) => {
addLog(`🚀 [navigateTo-invoke] 目标页面:${args.url || "未知"}`);
await delay(1000);
return false;
},
success: () => {
addLog("🚀 [navigateTo-success] 跳转成功");
},
fail: (err) => {
addLog(`🚀 [navigateTo-fail] 跳转失败:${err.errMsg || "未知错误"}`);
},
complete: () => {
addLog("🚀 [navigateTo-complete] 跳转操作完成");
},
});
navigateActive.value = true;
addLog("✅ navigateTo 拦截器已添加");
}
// 添加多个异步 navigateTo 拦截器
function addMultiAsyncNavigateInterceptor() {
if (navigateActive.value) {
addLog("navigateTo 拦截器已存在,无需重复添加");
return;
}
uni.addInterceptor("navigateTo", {
invoke: async (args) => {
addLog(`🚀 [navigateTo-invoke] 目标页面:${args.url || "未知"}`);
return new Promise((resolve) => {
setTimeout(() => {
addLog("s1-invoke");
resolve(args);
}, 2000);
});
},
success: () => {
addLog("🚀 [navigateTo-success] 跳转成功");
},
fail: (err) => {
addLog(`🚀 [navigateTo-fail] 跳转失败:${err.errMsg || "未知错误"}`);
},
complete: () => {
addLog("🚀 [navigateTo-complete] 跳转操作完成");
},
});
uni.addInterceptor("navigateTo", {
invoke: async (args) => {
addLog(`🚀 [navigateTo-invoke] 目标页面:${args.url || "未知"}`);
return new Promise((resolve) => {
setTimeout(() => {
addLog("s2-invoke");
resolve(args);
}, 4000);
});
},
success: () => {
addLog("🚀 [navigateTo-success] 跳转成功");
},
fail: (err) => {
addLog(`🚀 [navigateTo-fail] 跳转失败:${err.errMsg || "未知错误"}`);
},
complete: () => {
addLog("🚀 [navigateTo-complete] 跳转操作完成");
},
});
navigateActive.value = true;
addLog("✅ navigateTo 拦截器已添加");
}

// 移除 navigateTo 拦截器
function removeNavigateInterceptor() {
if (!navigateActive.value) {
addLog("navigateTo 拦截器未启用,无需移除");
return;
}
uni.removeInterceptor("navigateTo");
navigateActive.value = false;
addLog("🗑️ navigateTo 拦截器已移除");
}
// 触发跳转
function triggerNavigate() {
addLog("--- 手动触发 uni.navigateTo ---");
uni.navigateTo({
url: "/pages/index/index",
success: () => {
addLog("📋 跳转成功回调");
},
fail: (err) => {
addLog(`📋 跳转失败回调:${err.errMsg || "未知"}`);
},
});
}
</script>

<template>
<view class="container">
<text class="title">
拦截器示例
</text>

<!-- navigateTo 拦截器 -->
<view class="section">
<text class="section-title">
navigateTo 拦截器
</text>
<view class="btn-group">
<button class="btn add" @click="addNavigateInterceptor">
添加 navigateTo 拦截器
</button>
<button class="btn add" @click="addAsyncNavigateInterceptor">
添加异步 navigateTo 拦截器
</button>
<button class="btn add" @click="addMultiAsyncNavigateInterceptor">
添加多个异步 navigateTo 拦截器
</button>
<button class="btn remove" @click="removeNavigateInterceptor">
移除 navigateTo 拦截器
</button>
</view>
<text class="status">
状态:{{ navigateActive ? '✅ 已启用' : '❌ 未启用' }}
</text>
</view>

<!-- 触发操作 -->
<view class="section">
<text class="section-title">
触发操作
</text>
<view class="btn-group">
<button class="btn action" @click="triggerNavigate">
跳转页面
</button>
</view>
</view>
</view>
</template>

<style scoped lang="css">
.container {
padding: 20rpx;
}

.title {
font-size: 40rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}

.section {
margin-bottom: 30rpx;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 16rpx;
}

.section-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 16rpx;
}

.btn-group {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 12rpx;
}

.btn {
font-size: 24rpx;
padding: 12rpx 20rpx;
border-radius: 8rpx;
}

.btn.add {
background-color: #007aff;
color: #ffffff;
}

.btn.remove {
background-color: #ff3b30;
color: #ffffff;
}

.btn.action {
background-color: #34c759;
color: #ffffff;
}

.status {
font-size: 24rpx;
color: #666666;
}
</style>
65 changes: 65 additions & 0 deletions packages/uni-api/__tests__/helpers/interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
globalInterceptors,
invokeApi,
scopedInterceptors,
} from '../../src/helpers/interceptor'

function resetInterceptors() {
Object.keys(globalInterceptors).forEach((key) => {
delete globalInterceptors[key as keyof typeof globalInterceptors]
})
Object.keys(scopedInterceptors).forEach((key) => {
delete scopedInterceptors[key]
})
}

describe('interceptor', () => {
afterEach(() => {
resetInterceptors()
})

test('blocks api invocation when invoke returns false synchronously', () => {
const api = jest.fn()
globalInterceptors.invoke = [
() => {
return false
},
]

const res = invokeApi('test', api, {}, [])

expect(res).toBeUndefined()
expect(api).not.toHaveBeenCalled()
})

test('blocks api invocation when invoke resolves false asynchronously', async () => {
const api = jest.fn()
globalInterceptors.invoke = [
() => {
return Promise.resolve(false)
},
]

invokeApi('test', api, {}, [])
await new Promise((resolve) => setTimeout(resolve, 0))

expect(api).not.toHaveBeenCalled()
})

test('passes resolved options to api invocation', async () => {
const api = jest.fn((options) => options)
const options = { url: '/old' }
const resolvedOptions = { url: '/new' }
globalInterceptors.invoke = [
() => {
return Promise.resolve(resolvedOptions)
},
]

const res = await invokeApi('test', api, options, [])

expect(res).toBe(resolvedOptions)
expect(api).toHaveBeenCalledTimes(1)
expect(api).toHaveBeenCalledWith(resolvedOptions)
})
})
28 changes: 21 additions & 7 deletions packages/uni-api/src/helpers/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,24 @@ export type Interceptors = { [P in HOOKS]?: Function[] }
export const globalInterceptors: Interceptors = {}
export const scopedInterceptors: { [key: string]: Interceptors } = {}

const API_INVOKE_ABORT = {
then() {},
catch() {},
} as Promise<undefined>

function resolveHookResult(res: unknown, data?: unknown) {
return res === false ? API_INVOKE_ABORT : res || data
}

function wrapperHook(hook: Function, params?: Record<string, any>) {
return function (data: unknown) {
return hook(data, params) || data
const res = hook(data, params)
if (isPromise(res)) {
return Promise.resolve(res).then((res) => {
return resolveHookResult(res, data)
})
}
return resolveHookResult(res, data)
}
}

Expand All @@ -35,17 +50,16 @@ function queue(
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i]
if (promise) {
promise = Promise.resolve(wrapperHook(hook, params))
promise = promise.then(wrapperHook(hook, params))
} else {
const res = hook(data, params)
if (isPromise(res)) {
promise = Promise.resolve(res)
promise = Promise.resolve(res).then((res) => {
return resolveHookResult(res)
})
}
if (res === false) {
return {
then() {},
catch() {},
} as Promise<undefined>
return API_INVOKE_ABORT
}
}
}
Expand Down
Loading