鸿蒙平台 VSCodium 代码编辑器适配实战:基于 Electron 壳方案的轻量级 IDE 开发指南
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 Editor | CodeMirror 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 / .htm | htmlmixed |
| .js / .jsx | javascript |
| .ts / .tsx | typescript |
| .css / .scss | css(内置 scss) |
| .sass | sass |
| .py | python |
| .md | markdown |
| .json | json |
| .java | java |
| .c / .cpp | c / 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;
}
配色方案对照表:
| 元素 | 色值 | 说明 |
|---|---|---|
| 主背景 | #1e1e1e | VS Code 深色背景 |
| 工具栏/侧边栏 | #252526 | 稍浅的深色 |
| 边框 | #3c3c3c | 分隔线 |
| 按钮 | #0e639c | VS Code 蓝色按钮 |
| 状态栏 | #007acc | VS 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 技术栈本身,无需关心平台差异。
更多推荐




所有评论(0)