利用 WebMKS 和 Java 实现前端访问虚拟机网页

实战干货:手把手教你用 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 那里申请一个。

核心逻辑:

  1. 连上 ESXi。
  2. 靠 UUID 找到那台虚拟机。
  3. 申请一个类型叫 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 挺挑剔,这几点没做到肯定出问题:

  1. 必装 VMware Tools:不装的话,显卡驱动不行,画面传输慢甚至直接黑屏。
  2. 显存给够 :Windows 10 这种系统,显存起码给到 128MB
  3. 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>
相关推荐
文心快码BaiduComate2 小时前
插件开发实录:我用Comate在VS Code里造了一场“能被代码融化”的初雪
前端·后端·前端框架
嘻哈baby2 小时前
搞了三年运维,这些脚本我天天在用
前端
inCBle2 小时前
vue2 封装一个自动校验是否溢出的 tooltip 自定义指令
前端·javascript·vue.js
掘金安东尼2 小时前
⏰前端周刊第444期(2025年12月8日–12月14日)
前端
weixin_448119942 小时前
Datawhale Hello-Agents入门篇202512第2次作业
java·前端·javascript
程序员爱钓鱼2 小时前
Node.js 编程实战:路由与中间件
前端·后端·node.js
程序员爱钓鱼2 小时前
Node.js 编程实战:Express 基础
前端·后端·node.js
Cat God 0072 小时前
完整静态工具网站(尝试)
前端·html
WindrunnerMax2 小时前
从零实现富文本编辑器#9-编辑器文本结构变更的受控处理
前端·架构·github