实战干货:手把手教你用 WebMKS 和 Java 搞定 ESXi 虚拟机网页控制台
最近在折腾虚拟机网页控制台,发现用 VMware 原生的 WebMKS SDK 配合 ESXi 的 API 效果最好。这套方案不用装插件,直接在浏览器里就能操作。把中间踩过的坑和实现逻辑整理出来,希望能帮到大家。
1. 准备工作:依赖得放本地
WebMKS 这个 SDK 比较老派,它离不开 jQuery 。为了稳妥,别用 CDN,直接把这几个文件下载下来放到你项目的 public/static/wmks 目录下。
怎么引入?
在 index.html 里按这个顺序排好,jQuery 必须在最前面,不然 SDK 报错。
HTML
xml
<head>
<link rel="stylesheet" href="/static/wmks/css/wmks-all.css">
<script src="/static/wmks/jquery.min.js"></script>
<script src="/static/wmks/jquery-ui.min.js"></script>
<script src="/static/wmks/wmks.min.js"></script>
</head>
2. 后端:Java 怎么拿 Ticket
控制台连接前得先要个"门票"(Ticket)。后端用 Java 调用官方的 vijava 库,去 ESXi 那里申请一个。
核心逻辑:
- 连上 ESXi。
- 靠 UUID 找到那台虚拟机。
- 申请一个类型叫
webmks的票据。
Java
java
public TicketInfo getVmTicket(String host, String user, String pwd, String vmUuid) throws Exception {
// 建立连接
ServiceInstance si = new ServiceInstance(new URL("https://" + host + "/sdk"), user, pwd, true);
try {
InventoryNavigator nav = new InventoryNavigator(si.getRootFolder());
VirtualMachine vm = (VirtualMachine) nav.searchManagedEntity("VirtualMachine", vmUuid);
// 关键:拿 webmks 票据
VirtualMachineTicket vmt = vm.acquireTicket("webmks");
// 把票据、主机IP、端口发给前端
return new TicketInfo(vmt.getTicket(), vmt.getHost(), 443);
} finally {
si.getServerConnection().logout(); // 记得退出登录,不然会话堆积
}
}
3. 前端:Vue 3 里的核心控制
初始化连接
在 Vue 里的思路是:拿到票据后,立刻初始化 SDK 并连上 WebSocket。
TypeScript
typescript
const startWMKS = (ticket: string) => {
const WMKS_LIB = window.WMKS;
// 初始化配置
wmksInstance.value = WMKS_LIB.createWMKS("wmks-container", {
enableHints: true,
changeResolutionOnResizing: true, // 窗口变了,分辨率也跟着变
rescaleOnParentResize: true, // 自动缩放
position: 'absolute'
});
// 连上 ESXi 的 443 端口
wmksInstance.value.connect(`wss://${activeVM.value.host}:443/ticket/${ticket}`);
};
全屏怎么变清晰?
全屏时如果直接拉伸,画面会糊。我的办法是:先断开,变全屏,再重连。虽然多等了半秒,但画面会根据全屏后的尺寸重新协商,清晰度瞬间提升。
4. Windows 虚拟机的专项关照
搞 Linux 基本没啥事,但 Windows 挺挑剔,这几点没做到肯定出问题:
- 必装 VMware Tools:不装的话,显卡驱动不行,画面传输慢甚至直接黑屏。
- 显存给够 :Windows 10 这种系统,显存起码给到 128MB。
- CAD 按钮 :Windows 登录得按
Ctrl+Alt+Del。网页里按没用,必须给用户做个按钮调用wmksInstance.sendCAD()。
5. 样式:别留白边
为了让黑色背景铺满整个弹窗,CSS 得这么写:
SCSS
css
/* 弹窗样式 */
:deep(.vnc-session-dialog) {
display: flex;
flex-direction: column;
background: #000 !important;
/* 全屏模式强行占满 */
&.is-fullscreen {
height: 100vh !important;
.el-dialog__body { flex: 1; display: flex; flex-direction: column; }
}
/* 核心:让 body 自动撑开 */
.el-dialog__body {
flex: 1;
padding: 0 !important;
overflow: hidden;
}
}
总结一下
做这玩意儿逻辑其实不难,关键在于细节:
- 顺序:jQuery 必须先引。
- 时机:全屏重连能解决画面糊的问题。
- 补丁:给 Windows 留个 CAD 按钮,装好 VMware Tools。
只要这几点对齐了,剩下的就是调 UI 让它看着更顺手的事儿了。 最后放上图片和源码
js
<template>
<div class="vnc-app-wrapper">
<div class="page-header">
<div class="brand">
<el-icon :size="28" color="#409eff"><Monitor /></el-icon>
<h1>VMware 虚拟化资源管理</h1>
</div>
</div>
<div class="vm-grid">
<div v-for="(vm, index) in vmList" :key="index" class="vm-mini-card">
<div class="vm-status-bar" :class="vm.status"></div>
<div class="vm-card-body">
<div class="vm-icon">
<Platform v-if="vm.os === 'windows'" />
<Cpu v-else />
</div>
<div class="vm-details">
<h3 class="vm-name">{{ vm.name }}</h3>
<p class="vm-ip">{{ vm.host }}</p>
</div>
<div class="vm-actions">
<el-button type="primary" plain size="small" @click="openConsole(vm)">
连接控制台
</el-button>
</div>
</div>
<div class="vm-footer">
<span>UUID: {{ vm.vmUuid.substring(0, 14) }}...</span>
</div>
</div>
</div>
<el-dialog
v-model="vncDialogVisible"
:fullscreen="isFullscreen"
:width="isFullscreen ? '100%' : '1100px'"
top="5vh"
:draggable="true"
:show-close="false"
class="vnc-session-dialog"
:before-close="handleCloseVNC"
>
<template #header>
<div class="session-header drag-area">
<div class="session-info">
<div class="status-indicator" :class="connectionClass"></div>
<span class="session-name">
{{ activeVM?.name }} | {{ activeVM?.host }}
</span>
</div>
<div class="session-controls">
<el-button size="small" link @click="sendCAD">CAD</el-button>
<div class="divider"></div>
<el-button size="small" link @click="toggleFullscreen">
<el-icon :size="16">
<FullScreen v-if="!isFullscreen" />
<CopyDocument v-else />
</el-icon>
</el-button>
<el-button size="small" link type="danger" @click="handleCloseVNC">
<el-icon :size="16"><Close /></el-icon>
</el-button>
</div>
</div>
</template>
<div
class="session-stage"
v-loading="reloading"
element-loading-background="rgba(0, 0, 0, 0.9)"
element-loading-text="正在重构画面..."
>
<div id="wmks-container"></div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, computed, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { Monitor, FullScreen, Close, CopyDocument, Platform, Cpu } from '@element-plus/icons-vue'
import { getVmConsoleTicket } from '@/api/taskManagement/vmConsole'
const vmList = ref([
{
name: 'Windows-测试机',
host: '',
username: '',
password: '',
vmUuid: '',
os: 'windows',
status: 'online'
},
{
name: 'Linux-生产机',
host: '',
username: '',
password: '',
vmUuid: '',
os: 'linux',
status: 'online'
}
])
const activeVM = ref<any>(null)
const vncDialogVisible = ref(false)
const isFullscreen = ref(false)
const reloading = ref(false)
const vncStatus = ref('disconnected')
const wmksInstance = ref<any>(null)
const connectionClass = computed(() => ({
'is-connecting': vncStatus.value === 'connecting',
'is-connected': vncStatus.value === 'connected'
}))
const openConsole = (vm: any) => {
activeVM.value = vm;
handleConnect(false);
}
const toggleFullscreen = async () => {
reloading.value = true;
if (wmksInstance.value) {
wmksInstance.value.disconnect();
wmksInstance.value.destroy();
wmksInstance.value = null;
}
isFullscreen.value = !isFullscreen.value;
await nextTick();
setTimeout(async () => {
try { await handleConnect(true); } finally { reloading.value = false; }
}, 500);
}
const handleConnect = async (isRetry = false) => {
const win = window as any;
if (!win.WMKS) return ElMessage.error("SDK 未加载");
try {
vncStatus.value = 'connecting';
if (!isRetry) vncDialogVisible.value = true;
const res = await getVmConsoleTicket({
host: activeVM.value.host,
username: activeVM.value.username,
password: activeVM.value.password,
vmUuid: activeVM.value.vmUuid
});
const ticket = res.data?.ticket || res.ticket || res;
await nextTick();
startWMKS(ticket);
} catch (err) {
ElMessage.error("Ticket 申请失败,请检查机房连接");
vncStatus.value = 'disconnected';
}
}
const startWMKS = (ticket: string) => {
const WMKS_LIB = (window as any).WMKS;
wmksInstance.value = WMKS_LIB.createWMKS("wmks-container", {
enableHints: true,
useVNCHandshake: false,
changeResolutionOnResizing: true,
rescaleOnParentResize: true,
position: 'absolute'
});
wmksInstance.value.register(WMKS_LIB.CONST.Events.CONNECTION_STATE_CHANGE, (evt: any, data: any) => {
if (data.state === WMKS_LIB.CONST.ConnectionState.CONNECTED) {
vncStatus.value = 'connected';
}
});
wmksInstance.value.connect(`wss://${activeVM.value.host}:443/ticket/${ticket}`);
}
const sendCAD = () => wmksInstance.value?.sendCAD();
const handleCloseVNC = () => {
if (wmksInstance.value) {
wmksInstance.value.disconnect();
wmksInstance.value.destroy();
wmksInstance.value = null;
}
vncDialogVisible.value = false;
isFullscreen.value = false;
vncStatus.value = 'disconnected';
}
onBeforeUnmount(handleCloseVNC);
</script>
<style scoped lang="scss">
.vnc-app-wrapper {
padding: 40px;
background-color: #f8fafc;
min-height: 100vh;
}
.page-header {
max-width: 1200px;
margin: 0 auto 40px;
.brand {
display: flex; align-items: center; gap: 12px;
h1 { font-size: 22px; color: #334155; margin: 0; }
}
}
.vm-grid {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
.vm-mini-card {
background: #fff; border-radius: 12px; overflow: hidden;
border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
transition: transform 0.2s;
&:hover { transform: translateY(-4px); }
.vm-status-bar {
height: 4px; background: #cbd5e1;
&.online { background: #10b981; }
}
.vm-card-body {
padding: 24px; display: flex; flex-direction: column; align-items: center;
.vm-icon {
font-size: 32px; color: #3b82f6; margin-bottom: 16px;
padding: 12px; background: #eff6ff; border-radius: 12px;
}
.vm-name { font-size: 17px; font-weight: 600; color: #1e293b; margin: 0 0 4px; }
.vm-ip { font-size: 13px; color: #64748b; margin-bottom: 24px; }
.vm-actions { width: 100%; .el-button { width: 100%; } }
}
.vm-footer {
padding: 12px 20px; background: #f8fafc; border-top: 1px solid #f1f5f9;
font-size: 11px; color: #94a3b8; font-family: ui-monospace, SFMono-Regular;
}
}
:deep(.vnc-session-dialog) {
display: flex; flex-direction: column;
background: #000 !important; border-radius: 12px; overflow: hidden;
.el-dialog__header {
padding: 0 !important;
margin: 0 !important;
height: 50px;
cursor: move;
}
.el-dialog__body {
flex: 1; display: flex; flex-direction: column;
padding: 0 !important;
}
&.is-fullscreen { border-radius: 0; }
}
.session-header {
height: 50px; background: #1e293b; padding: 0 20px;
display: flex; justify-content: space-between; align-items: center;
.session-info {
display: flex; align-items: center; gap: 10px;
pointer-events: none;
.status-indicator {
width: 8px; height: 8px; border-radius: 50%; background: #64748b;
&.is-connected { background: #10b981; box-shadow: 0 0 8px #10b981; }
}
.session-name { color: #f1f5f9; font-size: 14px; font-weight: 500; }
}
.session-controls {
pointer-events: auto;
display: flex; align-items: center; gap: 10px;
.divider { width: 1px; height: 16px; background: #334155; }
.el-button { color: #94a3b8; &:hover { color: #fff; } }
}
}
.session-stage {
flex: 1; width: 100%; min-height: 600px;
background: #000; position: relative;
#wmks-container { width: 100% !important; height: 100% !important; position: absolute; }
}
:deep(.el-dialog__headerbtn) { display: none; }
</style>
这是index.html下面需要引入的内容
js
<link rel="stylesheet" href="/WebMKS_SDK_2.2.0/css/wmks-all.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
window.jQuery = window.$ = window.jQuery;
console.log('JQuery Check:', window.jQuery.fn.jquery);
</script>
<script src="/jquery-ui.min.js"></script>
<script src="/WebMKS_SDK_2.2.0/wmks.min.js"></script>
<script>
if (window.jQuery && (window.jQuery.ui || window.jQuery.widget)) {
console.log('Result: jQuery UI is fully loaded and attached to jQuery.');
} else {
console.error('Result: jQuery UI failed to attach to window.jQuery!');
}
</script>
