连接远程云应用时鼠标卡顿,性能优化问题
ztj100 2025-09-12 06:13 4 浏览 0 评论
guacamole远程桌面服务器应用。企业中遇到一个性能问题。就是连接远程桌面应用之后,鼠标会出现重影,卡顿问题。
<template>
<a-drawer
ref="cloudAppRef"
v-model:open="open"
:width="drawerWidth"
:mask-style="{ opacity: 0 }"
:body-style="{ padding: 0, overflow: 'hidden' }"
class="cloud-applications-container"
title="云应用"
placement="right"
@close="onClose"
>
<template #extra>
<ps-svg-icon v-if="isFullScreen" name="FullscreenExitOutlined" :size="16" class="extra-ico" @click="onFullscreen(false)" />
<ps-svg-icon v-else name="FullscreenOutlined" :size="16" class="extra-ico" @click="onFullscreen(true)" />
</template>
<div class="guacamole-wrapper">
<div ref="displayContainer" class="guacamole-display" tabindex="0" />
<div ref="mouseCapture" class="mouse-capture" @mousemove.stop @mousedown.stop @mouseup.stop />
</div>
</a-drawer>
</template>
<script setup lang="ts">
import PsSvgIcon from '@ps/svg-icon'
import Guacamole from 'guacamole-common-js'
import { computed, getCurrentInstance, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { queryAppTokenApi } from './api'
import { cloudApplicationsEmits, cloudApplicationsProps } from './cloud-applications'
import { setupI18n } from './locale'
defineOptions({
name: 'PsCloudApplications',
})
const props = defineProps(cloudApplicationsProps)
const emits = defineEmits(cloudApplicationsEmits)
// core
const App: any = getCurrentInstance()?.appContext.app
setupI18n(App)
const drawerWidth = ref('66%')
const cloudAppRef = ref()
const displayContainer = ref()
const mouseCapture = ref()
const isFullScreen = ref(false)
const guacamole = reactive({
client: null as any,
tunnel: null as any,
display: null as any,
mouse: null as any,
keyboard: null as any,
maxRetryCount: 5,
curRetryCount: 0,
mouseState: null as any,
localPointerHidden: false
})
const open = computed({
get() {
return props.appVisabled
},
set(value) {
emits('update:appVisabled', value)
},
})
const token = ref('')
const guacId = ref(1)
// 调整DPI为更合适的值,减少指针渲染负担
const getDpiValue = () => {
const screenDpi = window.devicePixelRatio || 1;
return Math.min(Math.max(96, 96 * screenDpi), 300); // 限制在96-300之间
}
// 新增:安全关闭连接的方法
const safeDisconnect = () => {
// 1. 先断开客户端连接(会自动关闭隧道)
if (guacamole.client) {
try {
guacamole.client.disconnect();
} catch (e) {
console.warn('客户端断开连接时出错:', e);
}
guacamole.client = null;
}
// 2. 仅在确认有close方法时才调用tunnel.close()
if (guacamole.tunnel && typeof guacamole.tunnel.close === 'function') {
try {
guacamole.tunnel.close();
} catch (e) {
console.warn('隧道关闭时出错:', e);
}
guacamole.tunnel = null;
} else {
// 对于没有close方法的版本,直接置空引用
guacamole.tunnel = null;
}
// 3. 清理输入设备
if (guacamole.mouse) {
try {
guacamole.mouse.unlisten();
} catch (e) {
console.warn('鼠标事件清理出错:', e);
}
guacamole.mouse = null;
}
if (guacamole.keyboard) {
try {
guacamole.keyboard.unlisten();
} catch (e) {
console.warn('键盘事件清理出错:', e);
}
guacamole.keyboard = null;
}
}
const initGuacamole = async () => {
// 先安全关闭可能存在的旧连接
safeDisconnect();
const width = displayContainer.value.clientWidth || 1920
const height = displayContainer.value.clientHeight || 1080
const dpi = getDpiValue();
// 精简URL参数,只保留必要的
guacamole.tunnel = new Guacamole.WebSocketTunnel(
`wss://guacamole.poissonsoft.com/guacamole/websocket-tunnel?` +
`token=${token.value}&` +
`GUAC_DATA_SOURCE=mysql&` +
`GUAC_ID=${guacId.value}&` +
`GUAC_TYPE=c&` +
`GUAC_WIDTH=${width}&` +
`GUAC_HEIGHT=${height}&` +
`GUAC_DPI=${dpi}&` +
`GUAC_TIMEZONE=Asia%2FShanghai`
)
guacamole.client = new Guacamole.Client(guacamole.tunnel)
guacamole.client.connect()
// 在连接成功后初始化输入设备
guacamole.client.onstatechange = (state: any) => {
if (state === 3) { // READY状态
// 显示远程桌面
guacamole.display = guacamole.client.getDisplay()
guacamole.display.getElement().style.zIndex = 'auto'
// 设置CSS加速渲染
const element = guacamole.display.getElement();
element.style.willChange = 'transform';
element.style.transform = 'translateZ(0)';
element.style.backfaceVisibility = 'hidden';
// 清空容器再添加,避免重复
displayContainer.value.innerHTML = '';
displayContainer.value.appendChild(element);
displayContainer.value.addEventListener('click', setFocus);
// 检查是否加载完毕
checkDisplayReady(changeSize);
// 绑定鼠标事件
initMouseHandling(element);
// 绑定键盘事件
initKeyboardHandling();
// 隐藏本地鼠标指针
hideLocalPointer(true);
} else if (state === 0) { // 断开连接状态
hideLocalPointer(false);
}
}
// 错误处理与重连机制
guacamole.tunnel.onerror = (error: any) => {
console.error('Tunnel error:', error);
hideLocalPointer(false);
if ([512, 514, 515, 769, 776].includes(error.code)) {
if (guacamole.curRetryCount < guacamole.maxRetryCount) {
setTimeout(initGuacamole, 2000);
guacamole.curRetryCount++;
}
}
}
}
// 改进鼠标事件处理,减少延迟
const initMouseHandling = (element: HTMLElement) => {
// 清除之前的鼠标实例
if (guacamole.mouse) {
guacamole.mouse.unlisten();
}
// 使用绝对定位的鼠标捕获层解决指针偏移问题
guacamole.mouse = new Guacamole.Mouse(mouseCapture.value);
// 使用requestAnimationFrame减少事件频率
const debounceMouseEvents = (callback: Function) => {
let isProcessing = false;
return (mouseState: any) => {
if (!isProcessing) {
requestAnimationFrame(() => {
callback(mouseState);
isProcessing = false;
});
isProcessing = true;
}
};
};
const handleMouse = debounceMouseEvents((mouseState: any) => {
guacamole.mouseState = mouseState;
guacamole.client.sendMouseState(mouseState, true);
});
guacamole.mouse.onmousedown = handleMouse;
guacamole.mouse.onmouseup = handleMouse;
guacamole.mouse.onmousemove = handleMouse;
}
// 改进键盘事件处理
const initKeyboardHandling = () => {
if (guacamole.keyboard) {
guacamole.keyboard.unlisten();
}
guacamole.keyboard = new Guacamole.Keyboard(displayContainer.value);
guacamole.keyboard.onkeydown = (keysym: any) => {
guacamole.client.sendKeyEvent(1, keysym);
};
guacamole.keyboard.onkeyup = (keysym: any) => {
guacamole.client.sendKeyEvent(0, keysym);
};
}
// 控制本地鼠标指针显示/隐藏
const hideLocalPointer = (hide: boolean) => {
if (displayContainer.value) {
displayContainer.value.style.cursor = hide ? 'none' : 'auto';
mouseCapture.value.style.cursor = hide ? 'none' : 'auto';
guacamole.localPointerHidden = hide;
}
}
const checkDisplayReady = (callback: any) => {
const timer = setInterval(() => {
if (guacamole.client?.getDisplay().getWidth() > 0) {
clearInterval(timer);
callback();
}
}, 100);
}
const changeSize = () => {
if (!guacamole.client) return;
const display = guacamole.client.getDisplay();
const container = displayContainer.value;
if (!display || !container) return;
const width = container.clientWidth;
const height = container.clientHeight;
const displayWidth = display.getWidth();
const displayHeight = display.getHeight();
if (displayWidth > 0 && displayHeight > 0) {
const scale = Math.min(
width / displayWidth,
height / displayHeight
).toFixed(4);
display.scale(scale);
if (isFullScreen.value) {
guacamole.client.sendSize(width, height);
}
}
}
const onFullscreen = (isFull: boolean) => {
isFullScreen.value = isFull;
drawerWidth.value = isFull ? '100%' : '66%';
requestAnimationFrame(() => {
setTimeout(changeSize, 300);
});
}
const setFocus = () => {
displayContainer.value?.focus();
}
const onClose = () => {
// 使用安全关闭方法
safeDisconnect();
hideLocalPointer(false);
open.value = false;
}
// 监听容器尺寸变化,自动调整
const handleResize = () => {
if (guacamole.client?.getDisplay()) {
changeSize();
}
};
onMounted(async () => {
const res = await queryAppTokenApi('cloudAppToken');
if (res?.data?.length) {
token.value = res.data[0].nameEn;
guacId.value = res.data[0].name || 1;
}
window.addEventListener('resize', handleResize);
setTimeout(initGuacamole, 100);
})
onUnmounted(() => {
displayContainer.value?.removeEventListener('click', setFocus);
window.removeEventListener('resize', handleResize);
// 组件卸载时安全关闭连接
safeDisconnect();
});
watch(open, (newVal) => {
if (newVal && guacamole.client?.getDisplay()) {
setTimeout(changeSize, 300);
}
});
</script>
<style scoped lang="scss">
@use 'cloud-applications';
.guacamole-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.guacamole-display {
width: 100%;
height: 100%;
min-height: 400px;
outline: none;
transform: translateZ(0);
will-change: transform;
}
.mouse-capture {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: auto;
}
.extra-ico {
margin-left: 8px;
cursor: pointer;
}
</style>
主要的优化点思路有:
1、通过CSS属性 transform: translateZ(0)、will-change等启用浏览器硬件加速,提升渲染性能。will-change属性会提前预告浏览器优化方向,预创建GP U层,分配显存,避免变化时卡顿;
2、启用RQ(requestAnimationFrame)来优化动画执行;
相关推荐
- sharding-jdbc实现`分库分表`与`读写分离`
-
一、前言本文将基于以下环境整合...
- 三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么
-
在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...
- MySQL8行级锁_mysql如何加行级锁
-
MySQL8行级锁版本:8.0.34基本概念...
- mysql使用小技巧_mysql使用入门
-
1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...
- MySQL/MariaDB中如何支持全部的Unicode?
-
永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...
- 聊聊 MySQL Server 可执行注释,你懂了吗?
-
前言MySQLServer当前支持如下3种注释风格:...
- MySQL系列-源码编译安装(v5.7.34)
-
一、系统环境要求...
- MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了
-
对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...
- MySQL字符问题_mysql中字符串的位置
-
中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...
- 深圳尚学堂:mysql基本sql语句大全(三)
-
数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...
- MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?
-
大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...
- 一文讲清怎么利用Python Django实现Excel数据表的导入导出功能
-
摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...
- 用DataX实现两个MySQL实例间的数据同步
-
DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...
- MySQL数据库知识_mysql数据库基础知识
-
MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...
- 如何为MySQL中的JSON字段设置索引
-
背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)