VSCodium 是 VS Code 的开源分支,以轻量、无遥测著称。本文记录将其核心编辑能力迁移到鸿蒙平台、基于 Electron 壳方案从零构建轻量级代码编辑器的完整流程——涵盖文件资源管理器、CodeMirror 语法高亮、HTML 实时预览、右键菜单管理等核心功能的实现细节与踩坑经验。

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_vscodium

一、技术架构分析

1.1 整体架构

本项目采用 Electron Web 层 + 鸿蒙 HAP 壳工程 的分层架构:

  • Electron Web 层:运行于 ArkWeb 引擎,提供完整的代码编辑能力(语法高亮、代码折叠、自动补全、文件管理等)
  • 鸿蒙 HAP 壳工程:通过 web_engine 模块加载 Web 应用,提供窗口管理、文件系统访问、屏幕自适应等系统能力

这种架构的核心优势在于:业务代码与平台完全解耦,Electron 部分可以在任何平台独立运行,鸿蒙壳仅负责适配和打包。

1.2 技术方案

对比项VS Code 原版鸿蒙适配版
编辑器内核Monaco EditorCodeMirror 5(轻量级替代)
UI 框架Electron + React纯 HTML/CSS/JavaScript
语法高亮TextMate 语法CodeMirror 内置语言模式
文件管理VS Code 内置自研文件树 + IPC 通信
部署方式exe/dmg 安装包HAP 包(鸿蒙应用)
运行环境Windows/macOS/Linux鸿蒙 PC(ArkWeb 引擎)

1.3 核心功能清单

功能模块具体能力
代码编辑100+ 语言语法高亮、代码折叠、自动补全、括号/标签自动闭合
文件管理打开文件夹、文件树浏览、新建文件/文件夹、重命名、删除
实时预览仅 HTML 文件支持分屏预览、iframe 安全渲染、可拖动分隔条
用户界面VS Code 深色主题、侧边栏宽度可调、状态栏信息显示
快捷键Ctrl+S 保存、Ctrl+O 打开、Ctrl+Space 自动补全

二、环境准备

2.1 开发环境要求

项目版本/信息
操作系统Windows 10/11
核心框架Electron (Node.js + Chromium)
技术栈HTML5/CSS3/Vanilla JavaScript
编辑器CodeMirror 5.65.16
目标设备鸿蒙 PC
目标架构arm64-v8a
开发工具DevEco Studio(鸿蒙官方 IDE)

2.2 项目结构

ohos_hap/
├── electron-apps/
│   └── vscodium/                  # VSCodium Electron 应用源码(开发目录)
│       ├── main.js                # Electron 主进程(窗口管理 + IPC 通信)
│       ├── renderer.js            # 渲染进程(编辑器逻辑 + 文件管理)
│       ├── index.html             # HTML 页面结构
│       ├── package.json           # 项目配置
│       └── styles/
│           └── vscodium.css       # VS Code 风格深色主题样式
├── web_engine/                    # 鸿蒙 web_engine 模块
│   └── src/main/resources/
│       └── resfile/resources/app/ # 部署目录(构建时打包进 HAP)
│           ├── main.js
│           ├── renderer.js
│           ├── index.html
│           ├── package.json
│           └── styles/vscodium.css
└── electron/                      # Electron 原生库
    └── libs/arm64-v8a/
        ├── libelectron.so         # Electron 核心库
        ├── libadapter.so          # 适配层库
        └── libffmpeg.so           # 多媒体库

开发流程:在 electron-apps/vscodium/ 中开发,每次修改后同步到 web_engine/src/main/resources/resfile/resources/app/ 部署目录。

三、核心适配流程

3.1 第一步:创建 Electron 主进程

文件:electron-apps/vscodium/main.js

主进程负责窗口创建和文件系统 IPC 通信,是整个应用的核心入口。

const { app, BrowserWindow, ipcMain, dialog, screen } = require('electron');
const fs = require('fs');
const path = require('path');

let mainWindow = null;

function createWindow() {
  const display = screen.getPrimaryDisplay();
  const { width, height } = display.workAreaSize;
  
  mainWindow = new BrowserWindow({
    width: Math.floor(width * 0.9),    // 屏幕宽度的 90%
    height: Math.floor(height * 0.85),  // 屏幕高度的 85%
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  });

  mainWindow.loadFile('index.html');
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  app.quit();
});

关键要点

  • 使用 screen.getPrimaryDisplay() 获取屏幕尺寸,实现窗口自适应屏幕大小
  • nodeIntegration: true 允许渲染进程使用 Node.js API
  • contextIsolation: false 简化 IPC 通信(生产环境建议开启隔离)

3.2 第二步:实现 IPC 通信层

主进程通过 ipcMain.handle 注册 IPC 通道,渲染进程通过 ipcRenderer.invoke 调用,实现文件系统的跨进程访问。

// ========== 文件对话框 ==========

// 打开文件
ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    filters: [{ name: '所有文件', extensions: ['*'] }]
  });
  return result;
});

// 打开文件夹
ipcMain.handle('dialog:openFolder', async () => {
  const result = await dialog.showOpenDialog(mainWindow, {
    properties: ['openDirectory'],
  });
  return result;
});

// 保存文件
ipcMain.handle('dialog:saveFile', async () => {
  const result = await dialog.showSaveDialog(mainWindow, {
    filters: [{ name: '所有文件', extensions: ['*'] }]
  });
  return result;
});

// ========== 文件读写 ==========

// 读取文件
ipcMain.handle('file:read', async (event, filePath) => {
  try {
    const content = fs.readFileSync(filePath, 'utf-8');
    return { success: true, content };
  } catch (err) {
    return { success: false, error: err.message };
  }
});

// 写入文件
ipcMain.handle('file:write', async (event, filePath, content) => {
  try {
    fs.writeFileSync(filePath, content, 'utf-8');
    return { success: true };
  } catch (err) {
    return { success: false, error: err.message };
  }
});

IPC 通道设计

通道名功能参数
dialog:openFile打开文件对话框
dialog:openFolder打开文件夹对话框
dialog:saveFile保存文件对话框
file:read读取文件内容filePath
file:write写入文件内容filePath, content
dir:read读取目录列表dirPath
file:create新建文件filePath
dir:create新建文件夹dirPath
file:rename重命名oldPath, newPath
file:delete删除文件/文件夹filePath

3.3 第三步:构建页面布局

文件:electron-apps/vscodium/index.html

页面采用经典的 VS Code 四区域布局:工具栏 + 侧边栏 + 编辑区 + 状态栏,并包含右键菜单的 HTML 结构。

<body>
  <!-- 顶部工具栏 -->
  <div class="toolbar">
    <div class="toolbar-left">
      <span class="logo">VSCodium</span>
      <button id="btn-open">打开</button>
      <button id="btn-save">保存</button>
    </div>
    <div class="toolbar-right">
      <button id="btn-preview">预览</button>
    </div>
  </div>

  <!-- 主内容区域 -->
  <div class="main">
    <!-- 左侧:文件资源管理器 -->
    <div class="sidebar" id="sidebar">
      <div class="sidebar-header">
        <span class="sidebar-title">资源管理器</span>
        <div class="sidebar-actions">
          <button class="sidebar-btn" id="btn-new-file" title="新建文件">📄</button>
          <button class="sidebar-btn" id="btn-new-folder" title="新建文件夹">📁</button>
        </div>
      </div>
      <div class="sidebar-content" id="fileTree">
        <div class="sidebar-empty">
          <p>未打开文件夹</p>
          <button id="btn-open-folder">打开文件夹</button>
        </div>
      </div>
    </div>

    <!-- 侧边栏拖动条 -->
    <div class="sidebar-resizer" id="sidebarResizer"></div>

    <!-- 中间:代码编辑区 -->
    <div class="editor-panel" id="editorPanel">
      <div class="editor-header">
        <span id="file-name">未命名</span>
      </div>
      <div class="editor-wrapper">
        <textarea id="editor"></textarea>
      </div>
    </div>

    <!-- 可拖动分隔条(预览模式显示) -->
    <div class="divider" id="divider"></div>

    <!-- 右侧:HTML 预览区 -->
    <div class="preview-panel" id="previewPanel" style="display: none;">
      <iframe id="preview"></iframe>
    </div>
  </div>

  <!-- 底部状态栏 -->
  <div class="statusbar">
    <span id="status-position">1,1</span>
    <span id="status-lang">HTML</span>
    <span id="status-file">未保存</span>
  </div>

  <!-- 右键菜单 -->
  <div class="context-menu" id="contextMenu" style="display: none;">
    <div class="context-menu-item" data-action="new-file">新建文件</div>
    <div class="context-menu-item" data-action="new-folder">新建文件夹</div>
    <div class="context-menu-separator"></div>
    <div class="context-menu-item" data-action="rename">重命名</div>
    <div class="context-menu-item" data-action="delete">删除</div>
  </div>
</body>

布局设计说明

  • 整体使用 flex 布局,body 为 flex-direction: column(垂直排列工具栏/主内容/状态栏)
  • .main 区域为 flex-direction: row(水平排列侧边栏/编辑区/预览区)
  • 侧边栏和编辑区之间、编辑区和预览区之间各有一个拖动条,支持宽度调整

3.4 第四步:集成 CodeMirror 代码编辑器

CodeMirror 是一个轻量级的代码编辑器组件,支持 100+ 编程语言的语法高亮。相比 VS Code 使用的 Monaco Editor,CodeMirror 体积更小、集成更简单,非常适合鸿蒙平台的轻量级场景。

引入 CodeMirror CDN 资源

<!-- 核心样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/theme/material-darker.css">

<!-- 核心库 -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>

<!-- 语言模式 -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/xml/xml.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/css/css.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/python/python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/markdown/markdown.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/clike/clike.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/sass/sass.min.js"></script>

<!-- 功能插件 -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/closebrackets.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/closetag.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/foldcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/foldgutter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/brace-fold.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/xml-fold.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/hint/show-hint.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/hint/html-hint.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/hint/css-hint.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/hint/javascript-hint.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/selection/active-line.min.js"></script>

初始化 CodeMirror 编辑器

editor = CodeMirror.fromTextArea(document.getElementById('editor'), {
  mode: 'htmlmixed',           // 默认 HTML 混合模式
  theme: 'material-darker',    // 深色主题
  lineNumbers: true,           // 显示行号
  autoCloseBrackets: true,     // 自动闭合括号
  autoCloseTags: true,         // 自动闭合 HTML 标签
  matchBrackets: true,         // 括号匹配高亮
  styleActiveLine: true,       // 当前行高亮
  foldGutter: true,            // 代码折叠
  gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
  indentUnit: 2,               // 缩进 2 空格
  tabSize: 2,
  extraKeys: {
    'Ctrl-S': saveFile,
    'Ctrl-O': openFile,
    'Ctrl-Space': 'autocomplete'  // Ctrl+Space 触发自动补全
  }
});

支持的语言模式

文件扩展名语言模式
.html / .htmhtmlmixed
.js / .jsxjavascript
.ts / .tsxtypescript
.css / .scsscss(内置 scss)
.sasssass
.pypython
.mdmarkdown
.jsonjson
.javajava
.c / .cppc / c++

3.5 第五步:实现文件资源管理器

文件资源管理器是类似 VSCode 体验的核心组件,支持目录树浏览、新建、重命名、删除等操作。

文件树渲染
async function renderFileTree() {
  const container = document.getElementById('fileTree');
  container.innerHTML = '';
  if (!rootFolder) return;

  const result = await ipcRenderer.invoke('dir:read', rootFolder);
  if (!result.success) return;

  // 渲染根文件夹名称
  const rootEl = document.createElement('div');
  rootEl.className = 'tree-item';
  rootEl.innerHTML = `
    <span class="arrow expanded">▶</span>
    <span class="icon">📂</span>
    <span class="name">${escapeHtml(path.basename(rootFolder))}</span>
  `;
  rootEl.addEventListener('click', (e) => {
    e.stopPropagation();
    toggleDir(rootFolder, rootEl);
  });
  container.appendChild(rootEl);

  // 渲染子项
  const childrenContainer = document.createElement('div');
  childrenContainer.className = 'tree-children';
  container.appendChild(childrenContainer);

  for (const item of result.items) {
    renderTreeItem(childrenContainer, item, rootFolder, 1);
  }
}

关键设计

  • 使用 escapeHtml() 转义文件名,防止特殊字符导致 XSS 问题
  • 目录按名称排序,文件夹优先显示
  • 自动过滤隐藏文件(以 . 开头)和 node_modules 目录
  • 支持递归展开/折叠子目录
右键菜单功能
function showContextMenu(e, target) {
  const menu = document.getElementById('contextMenu');
  contextTarget = target;
  menu.style.display = 'block';
  menu.style.left = e.clientX + 'px';
  menu.style.top = e.clientY + 'px';

  // 确保菜单不超出屏幕边界
  const rect = menu.getBoundingClientRect();
  if (rect.right > window.innerWidth) {
    menu.style.left = (window.innerWidth - rect.width - 5) + 'px';
  }
  if (rect.bottom > window.innerHeight) {
    menu.style.top = (window.innerHeight - rect.height - 5) + 'px';
  }
}

右键菜单支持四个操作:

操作说明
新建文件在当前目录下创建新文件,内联输入框输入名称
新建文件夹在当前目录下创建新文件夹
重命名内联输入框编辑名称,自动选中文件名(不含扩展名)
删除弹出确认对话框,支持删除文件和文件夹
内联新建/重命名

类似 VSCode 的体验,新建和重命名都使用内联输入框直接在文件树中编辑:

function createInline(parentPath, type, afterEl) {
  const container = document.getElementById('children-' + escapeId(parentPath));
  if (!container) return;

  const inputEl = document.createElement('div');
  inputEl.className = 'tree-item';
  inputEl.innerHTML = `
    <span class="arrow" style="visibility:hidden">▶</span>
    <span class="icon">${type === 'folder' ? '📁' : '📄'}</span>
    <input class="inline-input" type="text" 
           placeholder="${type === 'folder' ? '文件夹名称' : '文件名称'}" />
  `;
  container.insertBefore(inputEl, container.firstChild);

  const input = inputEl.querySelector('input');
  input.focus();

  // 回车确认创建
  input.addEventListener('keydown', async (e) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      const name = input.value.trim();
      if (!name) { inputEl.remove(); return; }
      const fullPath = path.join(parentPath, name);
      if (type === 'file') {
        await ipcRenderer.invoke('file:create', fullPath);
      } else {
        await ipcRenderer.invoke('dir:create', fullPath);
      }
      inputEl.remove();
      await refreshDir(parentPath);
    } else if (e.key === 'Escape') {
      inputEl.remove();  // ESC 取消
    }
  });
}

3.6 第六步:实现 HTML 实时预览

点击工具栏的"预览"按钮后,编辑区和预览区分屏显示,中间有可拖动的分隔条。预览功能仅对 HTML 文件生效——打开非 HTML 文件时,预览按钮会自动隐藏,避免渲染乱码。

预览按钮显隐控制

// 判断是否为 HTML 文件
function isHtmlFile(ext) {
  return ext === 'html' || ext === 'htm';
}

// 根据文件类型显示/隐藏预览按钮
function updatePreviewButton(ext) {
  const btn = document.getElementById('btn-preview');
  if (isHtmlFile(ext)) {
    btn.style.display = '';
    btn.disabled = false;
  } else {
    btn.style.display = 'none';
    // 隐藏时确保预览关闭
    if (isPreviewMode) {
      togglePreview();
    }
  }
}

在 loadFile 函数中,每次打开文件时调用 updatePreviewButton:

async function loadFile(filePath) {
  // ... 读取文件内容 ...
  const ext = path.extname(filePath).slice(1).toLowerCase();
  setLanguageMode(ext);
  // 根据文件类型控制预览按钮
  updatePreviewButton(ext);
  // 如果不是 HTML 且处于预览模式,退出预览
  if (!isHtmlFile(ext) && isPreviewMode) {
    togglePreview();
  }
}

预览切换逻辑

function togglePreview() {
  isPreviewMode = !isPreviewMode;
  const editorPanel = document.getElementById('editorPanel');
  const previewPanel = document.getElementById('previewPanel');
  const divider = document.getElementById('divider');

  if (isPreviewMode) {
    previewPanel.style.display = 'block';
    divider.style.display = 'block';
    // 计算可用宽度(减去侧边栏、侧边栏拖动条 4px、分隔条 5px)
    const mainWidth = document.querySelector('.main').offsetWidth;
    const sidebarWidth = document.getElementById('sidebar').offsetWidth;
    const sidebarResizerWidth = 4;
    const dividerWidth = 5;
    const available = mainWidth - sidebarWidth - sidebarResizerWidth - dividerWidth;
    const defaultWidth = Math.floor(available / 2);
    editorPanel.classList.add('preview-mode');
    editorPanel.style.width = defaultWidth + 'px';
    // 预览面板使用 flex:1 自动填满剩余空间
    previewPanel.style.flex = '1';
    previewPanel.style.width = 'auto';
    updatePreview();
  } else {
    previewPanel.style.display = 'none';
    divider.style.display = 'none';
    editorPanel.classList.remove('preview-mode');
    editorPanel.style.width = '';
    previewPanel.style.flex = '';
    previewPanel.style.width = '';
  }
  setTimeout(() => editor.refresh(), 100);
}

// 实时更新预览内容
function updatePreview() {
  if (isPreviewMode) {
    document.getElementById('preview').srcdoc = editor.getValue();
  }
}

预览按钮显示规则

场景预览按钮状态
初始启动(未打开文件)隐藏
打开 .html / .htm 文件显示,可正常使用
打开其他类型文件自动隐藏
预览模式中切换到非 HTML 文件自动退出预览并隐藏按钮

预览区使用 iframe + srcdoc

  • srcdoc 属性直接将 HTML 内容注入 iframe,无需写入临时文件
  • 每次编辑器内容变化时自动更新预览
  • 拖动分隔条时临时隐藏 iframe(visibility: hidden),防止 iframe 捕获鼠标事件导致拖动失效

3.7 第七步:CSS 深色主题样式

文件:electron-apps/vscodium/styles/vscodium.css

整体采用 VS Code 深色主题配色方案:

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #1e1e1e;     /* VS Code 深色背景 */
  color: #d4d4d4;          /* VS Code 文字颜色 */
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* 工具栏 */
.toolbar {
  height: 40px;
  background: #252526;     /* 工具栏背景 */
  border-bottom: 1px solid #3c3c3c;
}

/* 侧边栏 */
.sidebar {
  width: 220px;
  background: #252526;
  border-right: 1px solid #3c3c3c;
}

/* 状态栏 */
.statusbar {
  height: 22px;
  background: #007acc;     /* VS Code 蓝色状态栏 */
  color: white;
}

配色方案对照表

元素色值说明
主背景#1e1e1eVS Code 深色背景
工具栏/侧边栏#252526稍浅的深色
边框#3c3c3c分隔线
按钮#0e639cVS Code 蓝色按钮
状态栏#007accVS Code 蓝色状态栏
选中高亮#094771深蓝色选中背景
当前行#2c313a当前行高亮背景

3.8 第八步:侧边栏和分隔条拖动

侧边栏宽度和编辑区/预览区分隔条都支持鼠标拖动调整:

// 侧边栏拖动
function initSidebarResizer() {
  const resizer = document.getElementById('sidebarResizer');
  const sidebar = document.getElementById('sidebar');
  let isDragging = false;
  let startX = 0, startWidth = 0;

  resizer.addEventListener('mousedown', (e) => {
    isDragging = true;
    startX = e.clientX;
    startWidth = sidebar.offsetWidth;
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';
    e.preventDefault();
  });

  document.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    const dx = e.clientX - startX;
    const newWidth = startWidth + dx;
    if (newWidth >= 120 && newWidth <= 500) {
      sidebar.style.width = newWidth + 'px';
      editor.refresh();
    }
  });

  document.addEventListener('mouseup', () => {
    if (isDragging) {
      isDragging = false;
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
      editor.refresh();
    }
  });
}

拖动核心要点

  • 必须按住鼠标才能拖动(mousedown 设置 isDragging = true)
  • mousemove 中严格检查 isDragging 状态
  • 侧边栏宽度限制在 120px ~ 500px 之间
  • 拖动时设置 userSelect: none 防止文本被选中
  • 拖动结束后调用 editor.refresh() 让 CodeMirror 重新计算尺寸

四、文件同步部署

每次修改 electron-apps/vscodium/ 下的代码后,需要同步到鸿蒙 web_engine 部署目录:

# 清空部署目录
Remove-Item "web_engine\src\main\resources\resfile\resources\app\*" -Recurse -Force

# 复制最新文件
Copy-Item "electron-apps\vscodium\*" `
  -Destination "web_engine\src\main\resources\resfile\resources\app\" `
  -Recurse -Force

注意:每次修改代码后都必须同步,否则构建的 HAP 包不会包含最新代码。

五、常见问题与解决方案

Q1:CodeMirror 编辑器下半屏黑色空白

问题现象:编辑器代码只显示上半部分,下半屏是黑色空白区域,全屏时尤其明显

根本原因:CSS 中 .CodeMirror 使用了 position: absolute + height: auto 的组合,导致 CodeMirror 无法正确计算高度

解决方案:改用 height: 100% !important 明确填满容器

.editor-wrapper {
  flex: 1;
  position: relative;
  overflow: hidden;
  min-height: 0;
  background: #1e1e1e;  /* 背景色兜底 */
}

.CodeMirror {
  height: 100% !important;  /* 强制填满容器 */
  width: 100%;
  background: #1e1e1e;
}

同时需要在初始化后延迟调用 editor.refresh() 确保布局计算完成:

setTimeout(() => editor.refresh(), 200);
window.addEventListener('resize', () => editor.refresh());

Q2:预览区分隔条只能向左拖、不能向右拖

问题现象:分隔条拖动只能让编辑区变窄,无法让编辑区变宽

根本原因:编辑器面板使用 flex: 1,flex 布局会自动计算宽度并覆盖手动设置的 width

解决方案:使用 CSS class 切换方案

/* 默认状态:占满宽度 */
.editor-panel { flex: 1; }

/* 预览模式:固定宽度,允许拖动 */
.editor-panel.preview-mode { flex: none; }
// 进入预览模式
editorPanel.classList.add('preview-mode');   // flex: none

// 退出预览模式
editorPanel.classList.remove('preview-mode'); // flex: 1

Q3:预览区右侧出现黑色缝隙

问题现象:全屏模式下预览区右侧有一条黑色缝隙,小屏时不明显

根本原因:togglePreview 计算可用宽度时漏减了侧边栏拖动条 sidebar-resizer(4px)的宽度,导致编辑区 + 预览区总宽度超出实际可用空间

解决方案:补上遗漏的宽度,并让预览面板使用 flex: 1 自动填满剩余空间

const sidebarResizerWidth = 4;  // 补上遗漏的侧边栏拖动条宽度
const available = mainWidth - sidebarWidth - sidebarResizerWidth - dividerWidth;

// 预览面板自动填满剩余空间,消除缝隙
previewPanel.style.flex = '1';
previewPanel.style.width = 'auto';

Q4:拖动分隔条时预览区 iframe 干扰鼠标事件

问题现象:拖动分隔条时鼠标进入 iframe 区域后拖动失效

根本原因:iframe 会捕获鼠标事件,导致 mousemove 事件无法传递到分隔条

解决方案:拖动开始时隐藏 iframe,拖动结束后恢复

// mousedown 时隐藏
preview.style.visibility = 'hidden';

// mouseup 时恢复
preview.style.visibility = 'visible';

Q5:文件名包含特殊字符导致页面异常

问题现象:文件名包含 <、> 等 HTML 特殊字符时,文件树显示异常

根本原因:使用 innerHTML 拼接文件名时没有转义,存在 XSS 风险

解决方案:使用 escapeHtml() 函数转义所有动态内容

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// 使用
<span class="name">${escapeHtml(item.name)}</span>

Q6:如何同步文件到鸿蒙项目?

问题现象:修改了 electron-apps/vscodium/ 下的文件,但构建后没有生效

解决方案:使用 PowerShell 脚本一键同步

# 清空并复制
Remove-Item "web_engine\src\main\resources\resfile\resources\app\*" -Recurse -Force
Copy-Item "electron-apps\vscodium\*" `
  -Destination "web_engine\src\main\resources\resfile\resources\app\" `
  -Recurse -Force

部署路径:web_engine/src/main/resources/resfile/resources/app/

每次修改代码后都需要同步,否则构建的 HAP 包不包含最新代码。

六、总结

本文完整记录了 VSCodium 代码编辑器在鸿蒙 PC 平台的适配过程。核心技术要点总结如下:

技术点方案
编辑器内核CodeMirror 5(CDN 引入,轻量级)
语法高亮CodeMirror 内置语言模式(100+ 语言)
文件管理自研文件树 + Electron IPC 跨进程通信
实时预览iframe srcdoc 注入 + 仅 HTML 文件显示预览按钮
布局方案Flex 布局 + CSS class 切换
拖动功能mousedown/mousemove/mouseup 事件链
主题配色VS Code 深色主题色值方案
安全防护escapeHtml 转义 + iframe 隔离

整个适配过程遵循 Electron 壳方案标准化流程:在 electron-apps/ 开发目录中编写代码,通过同步脚本部署到 web_engine/ 模块,最终由鸿蒙壳工程打包为 HAP 安装包。这种流程使得 Web 应用的业务代码与鸿蒙平台完全解耦,开发者可以专注于 Web 技术栈本身,无需关心平台差异。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐