Tauri Webview 链接点击无响应问题排查与解决
问题描述
在开发 Tauri 桌面应用时,我们遇到了一个常见问题:在嵌入的 Webview 中点击网页链接时没有响应。具体表现是:
- Webview 可以正常加载百度首页
- 点击搜索结果或其他链接时,页面没有任何反应
- 控制台出现权限错误提示
问题背景
我们的应用使用 Tauri 2.0 框架,在一个独立的窗口中嵌入了一个 Webview 来显示百度搜索页面。代码实现如下:
import { Webview } from '@tauri-apps/api/webview'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { openUrl } from '@tauri-apps/plugin-opener'
const webviewContainer = ref<HTMLElement | null>(null)
const externalWebview = ref<Webview | null>(null)
const webviewLabel = 'baidu-webview'
const currentWindow = getCurrentWebviewWindow()
const initWebview = async () => {
await nextTick()
if (!webviewContainer.value) {
console.error('webviewContainer 未找到')
return
}
const windowInstance = getCurrentWebviewWindow()
try {
const existing = await Webview.getByLabel(webviewLabel)
if (existing) {
await existing.close()
}
} catch (error) {
console.log('没有找到已存在的 webview')
}
try {
const rect = webviewContainer.value.getBoundingClientRect()
const newWebview = new Webview(windowInstance, webviewLabel, {
url: 'https://www.baidu.com',
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
focus: true,
dragDropEnabled: true
})
externalWebview.value = newWebview
newWebview.once('tauri://created', async () => {
console.log('Webview 创建成功')
await updateWebviewBounds()
})
newWebview.once('tauri://error', (error) => {
console.error('Webview 创建失败:', error)
externalWebview.value = null
})
} catch (error) {
console.error('创建 webview 失败:', error)
console.log('尝试在系统浏览器中打开百度')
try {
await openUrl('https://www.baidu.com')
} catch (openError) {
console.error('在浏览器中打开失败:', openError)
}
}
}
错误信息
点击链接时,控制台出现以下错误:
Uncaught (in promise) shell.open not allowed on window "baidu", webview "baidu-webview", URL: `https://www.baidu.com/`
allowed on: [windows: "*", URL: local], [windows: "*", URL: local]
referenced by: capability: default-capability, permission: allow-open || capability: default-capability, permission: allow-open
问题分析
初步排查
首先,我尝试添加导航事件监听器来拦截和处理链接点击:
const handleNavigation = async (event: any) => {
const url = event.payload?.url || event.url
if (!url) return
console.log('导航事件:', url)
try {
const currentUrl = new URL(url)
const baiduUrl = new URL('https://www.baidu.com')
if (currentUrl.hostname !== baiduUrl.hostname) {
event.preventDefault()
await openUrl(url)
}
} catch (error) {
console.error('处理导航失败:', error)
}
}
newWebview.once('tauri://created', async () => {
console.log('Webview 创建成功')
await updateWebviewBounds()
try {
navigationUnlisten = await newWebview.onNavigationRequested(handleNavigation)
console.log('导航事件监听器已注册')
} catch (error) {
console.error('注册导航监听器失败:', error)
}
})
但是这个方案导致了同样的权限错误。
深入分析
仔细观察错误信息,我发现了关键线索:
- 错误提示
shell.open not allowed- 这表明是 shell 插件的权限问题 allowed on: [windows: "*", URL: local]- 只允许 local URL- 但代码中使用的是
@tauri-apps/plugin-opener的openUrl函数
这说明问题不是 shell:open 的权限,而是 opener 插件的权限配置不完整。
检查权限配置
查看 src-tauri/capabilities/default.json 文件:
{
"permissions": [
// ... 其他权限
"opener:default",
"opener:allow-reveal-item-in-dir",
"opener:allow-open-path",
// 缺少 opener:allow-open-url
"mic-recorder:default",
// ...
]
}
发现问题所在:配置中只有 opener:allow-open-path(用于打开本地文件路径),缺少 opener:allow-open-url(用于打开外部 URL)。
解决方案
1. 添加缺失的权限
在 src-tauri/capabilities/default.json 中添加 opener:allow-open-url 权限:
{
"permissions": [
// ... 其他权限
"opener:default",
"opener:allow-reveal-item-in-dir",
"opener:allow-open-path",
"opener:allow-open-url", // 新增这一行
"mic-recorder:default",
// ...
]
}
2. 移除导航拦截代码
由于我们已经添加了正确的权限,不再需要手动拦截导航事件。移除之前添加的导航拦截代码,让 Webview 按照默认行为工作。
3. 重新编译
修改权限配置后,需要重新编译 Tauri 应用:
# 开发环境
pnpm dev
# 或者
cd src-tauri
cargo tauri dev
最终代码
优化后的代码更加简洁,移除了不必要的导航拦截逻辑:
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { Webview } from '@tauri-apps/api/webview'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { openUrl } from '@tauri-apps/plugin-opener'
const webviewContainer = ref<HTMLElement | null>(null)
const externalWebview = ref<Webview | null>(null)
const webviewLabel = 'baidu-webview'
const currentWindow = getCurrentWebviewWindow()
let containerResizeObserver: ResizeObserver | null = null
let windowResizeListener: (() => void) | null = null
const updateWebviewBounds = async () => {
if (!webviewContainer.value || !externalWebview.value) return
try {
const rect = webviewContainer.value.getBoundingClientRect()
await externalWebview.value.setPosition(rect.left, rect.top)
await externalWebview.value.setSize(rect.width, rect.height)
} catch (error) {
console.error('更新 webview 边界失败:', error)
}
}
const initWebview = async () => {
await nextTick()
if (!webviewContainer.value) {
console.error('webviewContainer 未找到')
return
}
const windowInstance = getCurrentWebviewWindow()
try {
const existing = await Webview.getByLabel(webviewLabel)
if (existing) {
await existing.close()
}
} catch (error) {
console.log('没有找到已存在的 webview')
}
try {
const rect = webviewContainer.value.getBoundingClientRect()
const newWebview = new Webview(windowInstance, webviewLabel, {
url: 'https://www.baidu.com',
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
focus: true,
dragDropEnabled: true
})
externalWebview.value = newWebview
containerResizeObserver = new ResizeObserver(() => {
updateWebviewBounds()
})
containerResizeObserver.observe(webviewContainer.value)
windowResizeListener = () => {
updateWebviewBounds()
}
window.addEventListener('resize', windowResizeListener, { passive: true })
newWebview.once('tauri://created', async () => {
console.log('Webview 创建成功')
await updateWebviewBounds()
})
newWebview.once('tauri://error', (error) => {
console.error('Webview 创建失败:', error)
externalWebview.value = null
})
} catch (error) {
console.error('创建 webview 失败:', error)
console.log('尝试在系统浏览器中打开百度')
try {
await openUrl('https://www.baidu.com')
} catch (openError) {
console.error('在浏览器中打开失败:', openError)
}
}
}
onMounted(async () => {
await initWebview()
})
onUnmounted(async () => {
if (containerResizeObserver) {
containerResizeObserver.disconnect()
}
if (windowResizeListener) {
window.removeEventListener('resize', windowResizeListener)
}
if (externalWebview.value) {
try {
await externalWebview.value.close()
} catch (error) {
console.error('关闭 webview 失败:', error)
}
}
})
关键知识点
Tauri 权限系统
Tauri 2.0 使用基于 Capability 的权限系统,每个功能都需要在 capabilities/*.json 文件中明确授权:
- opener:allow-open-path: 用于打开本地文件路径
- opener:allow-open-url: 用于打开外部 URL
- shell:allow-open: 用于执行系统命令
Webview 导航行为
在 Tauri 中,Webview 的导航行为默认是自由的,不需要手动拦截。只有在需要特殊处理(如阻止某些导航、重定向到外部浏览器等)时,才需要使用 onNavigationRequested 事件。
常见错误信息解读
shell.open not allowed on window "xxx", webview "xxx", URL: `https://xxx/`
allowed on: [windows: "*", URL: local]
这个错误信息的含义:
shell.open not allowed: shell 插件的 open 命令不被允许URL: local: 只允许 local 类型的 URL- 需要检查对应的权限配置是否完整
总结
-
问题根源: 使用了
@tauri-apps/plugin-opener的openUrl函数,但权限配置中缺少opener:allow-open-url -
解决方法: 在
capabilities/default.json中添加opener:allow-open-url权限 -
最佳实践:
- 仔细阅读错误信息,理解权限限制
- 区分不同插件的权限需求(shell vs opener)
- 优先使用默认行为,避免过度拦截导航事件
- 修改权限配置后记得重新编译应用
-
调试技巧:
- 查看控制台的完整错误信息
- 检查
capabilities/*.json文件的权限配置 - 确认使用的 API 对应的权限要求
- 重新编译以应用权限更改
通过这次排查,我们不仅解决了当前问题,还深入理解了 Tauri 的权限系统,为后续开发积累了宝贵经验。
参考资料
更多推荐



所有评论(0)