鸿蒙平台 GnuWin32 Unix 工具集适配实战:基于 Electron 壳方案的 14 款文本处理工具真实实现

GnuWin32 是将经典 Unix 命令行工具集移植到桌面环境的开源项目,涵盖 grep、sed、awk、sort、uniq、diff 等 14 款核心文本处理工具。本文记录将 GnuWin32 基于 Electron 壳方案适配到鸿蒙 PC 平台的完整流程,通过 IPC 架构将 13 款工具的真实 Node.js 实现放在主进程执行,1 款前端工具在渲染进程本地模拟,所有功能零外部依赖、零模拟数据,全部基于真实输入文本处理。

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

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

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

一、技术架构分析

1.1 功能定位

GnuWin32 的核心价值在于将 Unix/Linux 系统中最常用的命令行文本处理工具集封装为一个图形化应用。与在终端中逐个输入命令不同,本方案提供了统一的侧栏工具列表 + 输入/输出双面板界面,每个工具都预置了可直接执行的示例数据,用户选择工具后点击执行即可看到真实处理结果。

1.2 目标架构(鸿蒙 Electron)

  • 技术栈:Electron + HTML/CSS/JavaScript + 鸿蒙 web_engine 模块
  • 核心逻辑:tools.js 使用 Node.js 内置模块(fs/path/os)实现 13 款工具的真实逻辑
  • IPC 通信:渲染进程通过 ipcRenderer.invoke 调用主进程的工具执行、文件读写、目录浏览等功能
  • awk 本地模拟:渲染进程内实现 print 表达式解析,覆盖 80% 的字段提取场景

1.3 架构设计

层级 职责 技术实现
主进程 main.js 窗口管理 + IPC 路由 Electron BrowserWindow + 6 个 IPC handler
工具引擎 tools.js 13 款工具真实实现 Node.js fs/path/os 内置模块
渲染进程 renderer.js UI 交互 + 选项渲染 + 示例加载 原生 DOM + ipcRenderer.invoke
前端工具 awk 字段提取本地模拟 渲染进程 print 表达式解析
样式层 gnuwin32.css 深色主题 Catppuccin Mocha 18 色变量

1.4 工具分类清单

本方案实现了 14 款工具,分为三大类别:

类别 工具 说明 执行位置
文本处理 grep 正则模式搜索,支持 -i/-n/-v/-c 主进程 IPC
文本处理 sed 流编辑器,支持任意分隔符和转义 主进程 IPC
文本处理 awk 字段提取(print 表达式) 渲染进程本地
文本处理 cut 按分隔符切割并提取指定列 主进程 IPC
文本处理 tr 字符替换或删除 主进程 IPC
文本处理 wc 行数/单词数/字符数统计 主进程 IPC
文件操作 find 目录递归搜索,支持名称和类型过滤 主进程 IPC
文件操作 diff 逐行对比两段文本差异 主进程 IPC
文件操作 cat 连接显示文本或文件内容 主进程 IPC
文件操作 tee 为每行添加行号标记 主进程 IPC
排序去重 sort 字母/数值排序,支持逆序 主进程 IPC
排序去重 uniq 相邻重复行过滤,支持计数 主进程 IPC
排序去重 head 提取前 N 行 主进程 IPC
排序去重 tail 提取后 N 行 主进程 IPC

1.5 鸿蒙平台适配要点

鸿蒙 Electron 适配层存在三大兼容约束,必须在开发中严格遵守:

  • 禁止使用原生 prompt/confirm/alert 对话框,调用会导致 SubWindow 崩溃
  • 禁止使用原生 select 元素,调用会触发 SubWindow 崩溃
  • 禁止使用 setWindowOpenHandler 和 will-navigate API,调用会导致页面纯白

本方案使用自定义 div 通知组件替代所有对话框,所有选项均使用 checkbox 和 text input 构建,完全规避了原生元素调用。

二、环境准备

2.1 开发环境要求

  • 操作系统:Windows 10/11
  • 开发工具:DevEco Studio(鸿蒙官方 IDE)
  • HarmonyOS SDK:API 21+(5.0.5+)
  • Node.js:v20+

2.2 项目结构

bash 复制代码
ohos_hap/
├── electron-apps/
│   └── GnuWin32/                # GnuWin32 应用源码
│       ├── main.js              # Electron 主进程(109 行)
│       ├── tools.js             # 工具引擎(420 行,13 款工具实现)
│       ├── renderer.js          # 渲染进程(587 行,UI + IPC + 示例)
│       ├── index.html           # 侧栏 + 双面板布局(120 行)
│       ├── package.json         # 项目配置
│       └── styles/
│           └── gnuwin32.css     # Catppuccin Mocha 深色主题(387 行)
├── web_engine/                  # 鸿蒙 web_engine 模块
│   └── src/main/resources/
│       └── resfile/resources/app/  # 部署目录
└── build-profile.json5          # 鸿蒙构建配置

三、核心适配流程

3.1 第一步:创建工具引擎

文件:electron-apps/GnuWin32/tools.js

工具引擎是整个应用的核心,420 行代码实现了 13 款工具的真实 Node.js 逻辑。所有工具仅使用 fs、path、os 三个内置模块,零外部依赖。

以 grep 工具为例,支持正则匹配、大小写忽略、行号显示、反向匹配、仅计数五种模式:

js 复制代码
// tools.js grep 工具完整实现
  grep: function(input) {
    var pattern = input.pattern || '';
    var text = input.text || '';
    var filePaths = parsePaths(input.filePath);
    var ignoreCase = !!input.ignoreCase;
    var lineNumber = input.lineNumber !== false;
    var invertMatch = !!input.invertMatch;
    var countOnly = !!input.countOnly;

    if (!pattern) throw new Error('请输入搜索模式(pattern)');
    var re;
    try { re = new RegExp(pattern, ignoreCase ? 'gi' : 'g'); }
    catch (e) { throw new Error('无效正则: ' + e.message); }

    var sources = filePaths.length > 0 ? [] : [{ name: '<stdin>', content: text }];
    if (filePaths.length > 0) {
      for (var fi = 0; fi < filePaths.length; fi++) {
        try {
          var stat = fs.statSync(filePaths[fi]);
          if (stat.isDirectory()) {
            var entries = fs.readdirSync(filePaths[fi]);
            for (var ei = 0; ei < entries.length; ei++) {
              var fp = path.join(filePaths[fi], entries[ei]);
              try { sources.push({ name: fp, content: fs.readFileSync(fp, 'utf-8') }); } catch (x) {}
            }
          } else {
            sources.push({ name: filePaths[fi], content: fs.readFileSync(filePaths[fi], 'utf-8') });
          }
        } catch (e) {
          sources.push({ name: filePaths[fi], content: '[Error: ' + e.message + ']' });
        }
      }
    }

    var output = [];
    var totalMatches = 0;
    for (var si = 0; si < sources.length; si++) {
      var src = sources[si];
      var lines = src.content.split('\n');
      var matches = 0;
      for (var li = 0; li < lines.length; li++) {
        var matched = re.test(lines[li]);
        re.lastIndex = 0;
        if (matched !== invertMatch) {
          matches++;
          totalMatches++;
          if (!countOnly) {
            var prefix = lineNumber ? (li + 1) + ':' : '';
            if (sources.length > 1) prefix = src.name + ':' + prefix;
            output.push(prefix + lines[li]);
          }
        }
      }
      if (countOnly) {
        var cPrefix = sources.length > 1 ? src.name + ':' : '';
        output.push(cPrefix + matches);
      }
    }
    return {
      output: output.join('\n') || '(无匹配结果)',
      stats: '匹配 ' + totalMatches + ' 行,扫描 ' + sources.length + ' 个源'
    };
  },

统一执行入口通过工具名称动态路由,简洁高效:

js 复制代码
tools.execute = function(name, input) {
  if (!tools[name]) throw new Error('未知工具: ' + name);
  return tools[name](input || {});
};

module.exports = tools;

3.2 第二步:搭建 IPC 通信层

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

主进程提供 6 个 IPC handler,覆盖工具执行、文件读写、目录浏览和系统对话框四大功能:

js 复制代码
// 工具执行
ipcMain.handle('tool:execute', async function(event, name, input) {
  try {
    var result = tools.execute(name, input);
    return { ok: true, output: result.output, stats: result.stats };
  } catch (e) {
    return { ok: false, error: e.message };
  }
});

// 文件读取
ipcMain.handle('fs:readFile', async function(event, filePath) {
  try {
    var content = fs.readFileSync(filePath, 'utf-8');
    return { ok: true, content: content };
  } catch (e) {
    return { ok: false, error: e.message };
  }
});

// 系统文件选择对话框
ipcMain.handle('dialog:openFile', async function() {
  try {
    var result = await dialog.showOpenDialog(mainWindow, {
      properties: ['openFile'],
      filters: [{ name: 'All Files', extensions: ['*'] }]
    });
    if (result.canceled) return { ok: false };
    return { ok: true, path: result.filePaths[0] };
  } catch (e) {
    return { ok: false, error: e.message };
  }
});

渲染进程通过 ipcRenderer.invoke 发起调用,非 Electron 环境下自动降级为错误提示:

js 复制代码
var hasNodeEnv = (typeof process !== 'undefined') && (typeof require === 'function');
var ipcRenderer = hasNodeEnv
  ? require('electron').ipcRenderer
  : { invoke: function() { return Promise.resolve({ ok: false, error: '非 Electron 环境' }); } };

3.3 第三步:设计双面板布局

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

界面采用左侧工具栏 + 右侧输入/输出双面板的经典布局。左侧按功能分为文本处理、文件操作、排序去重三个分类,右侧包含选项栏、输入区、可拖拽分隔条和输出区。

js 复制代码
<nav class="sidebar">
  <div class="sidebar-header">
    <span class="logo">🔧</span>
    <span class="logo-text">GnuWin32</span>
  </div>
  <div class="sidebar-search">
    <input type="text" class="search-input" id="toolSearch" placeholder="搜索工具...">
  </div>
  <div class="tool-categories" id="toolCategories">
    <div class="category-label">文本处理</div>
    <ul class="tool-list">
      <li class="tool-item active" data-tool="grep">
        <span class="tool-icon">🔍</span><span class="tool-name">grep</span><span class="tool-desc">模式搜索</span>
      </li>
      <!-- sed / awk / cut / tr / wc -->
    </ul>
    <div class="category-label">文件操作</div>
    <!-- find / diff / cat / tee -->
    <div class="category-label">排序去重</div>
    <!-- sort / uniq / head / tail -->
  </div>
</nav>

输入和输出面板之间放置了一个可拖拽分隔条,用户可以自由调整两个面板的比例:

js 复制代码
<div class="panel-resize" id="panelResize"></div>

输出面板标题区包含执行耗时统计和一键复制按钮:

js 复制代码
<div class="panel-header">
  <span class="panel-title">输出</span>
  <div class="output-header-right">
    <span class="exec-stats" id="execStats"></span>
    <button class="btn-copy" id="btnCopy" title="复制输出内容">📋 复制</button>
  </div>
</div>

3.4 第四步:实现工具切换与示例加载

文件:electron-apps/GnuWin32/renderer.js

每个工具在 TOOL_DEFS 中定义了标题、man 手册、选项列表和两组预置示例。切换工具时自动加载示例 1 并更新行数显示:

js 复制代码
function selectTool(name) {
  if (!TOOL_DEFS[name]) return;
  currentTool = name;

  // 更新侧栏高亮
  var items = document.querySelectorAll('.tool-item');
  for (var i = 0; i < items.length; i++) items[i].classList.toggle('active', items[i].dataset.tool === name);

  // 更新标题栏
  var def = TOOL_DEFS[name];
  el('toolBadge').textContent = name;
  el('viewTitle').textContent = def.title;
  el('toolMan').textContent = def.man;

  // 重建选项面板
  renderOptions(def.options);

  // 清空输入输出
  el('inputText').value = '';
  el('fileInfo').textContent = '';
  el('outputContent').innerHTML = '<div class="output-placeholder">点击「执行」按钮运行工具</div>';
  el('execStats').textContent = '';
  el('statusLeft').textContent = '工具: ' + name;

  // 自动加载示例并更新行数
  loadExample(1);
  updateLineCount();
}

示例加载函数将预置文本填入输入区,并将示例中的选项值映射到对应的 DOM 元素:

js 复制代码
function loadExample(num) {
  var def = TOOL_DEFS[currentTool];
  var exKey = 'example' + num;
  var ex = def[exKey];
  if (!ex) return;

  // 加载文本到输入区
  if (currentTool === 'diff') {
    el('inputText').value = '--- 文本 A ---\n' + (ex.textA || '') + '\n\n--- 文本 B ---\n' + (ex.textB || '');
  } else {
    el('inputText').value = ex.text || '';
  }
  el('fileInfo').textContent = '已加载示例: ' + (ex.label || exKey);
  // 加载选项值
  var opts = def.options || [];
  for (var i = 0; i < opts.length; i++) {
    var o = opts[i];
    var elInput = el(o.id);
    if (!elInput) continue;
    var exVal = ex[o.id.replace('opt', '').toLowerCase()] !== undefined
      ? ex[o.id.replace('opt', '').toLowerCase()]
      : ex[o.label.toLowerCase().replace(/[^a-z0-9]/g, '')];
    // 用选项 ID 匹配示例中的 key
    var shortKey = o.id.replace(/^opt/, '').replace(/([A-Z])/g, function(m) { return '_' + m.toLowerCase(); }).replace(/^_/, '');
    if (ex[shortKey] !== undefined) exVal = ex[shortKey];
    if (exVal === undefined) continue;
    if (o.type === 'check') { elInput.checked = !!exVal; }
    else { elInput.value = String(exVal); }
  }
}

3.5 第五步:实现自定义通知组件

鸿蒙平台禁止使用原生 alert/confirm/prompt,本方案使用纯 DOM 构建通知弹窗:

js 复制代码
function notify(title, msg, type) {
  var old = el('notification');
  if (old) old.remove();
  var div = document.createElement('div');
  div.id = 'notification';
  div.className = 'gw-notification' + (type === 'error' ? ' error' : type === 'success' ? ' success' : '');
  div.innerHTML = '<div class="notification-title">' + escapeHtml(title) + '</div>' +
    '<div class="notification-msg">' + escapeHtml(msg) + '</div>';
  document.body.appendChild(div);
  setTimeout(function() { if (div.parentNode) div.remove(); }, 4000);
}

通知支持三种类型:默认(蓝色)、error(红色)、success(绿色),4 秒后自动消失。

四、关键技术点

4.1 sed 解析器重写:支持转义和任意分隔符

sed 表达式的标准格式是 s/pattern/replacement/flags,但当正则中包含 / 字符时需要使用 / 转义。原始的简单正则解析方案无法正确处理转义场景:

js 复制代码
// 旧方案:非贪婪 .+? 在遇到第一个 / 时就停止匹配
var m = /^s\/(.+?)\/(.*)\/([gi]*)$/.exec(expression);
// 问题:s/\/(\d+)/replacement/g 会被错误解析

新方案采用手动逐字符解析,正确处理 \ 转义,并支持任意分隔符(/、#、| 等):

js 复制代码
// 新方案:支持转义 + 任意分隔符
var delim = expression.charAt(1);  // 自动检测分隔符
var body = expression.substring(2);
var segments = []; var seg = '';
for (var ci = 0; ci < body.length; ci++) {
  if (body.charAt(ci) === '\\' && ci + 1 < body.length) {
    // 转义字符:将当前和下一个字符一起加入段
    seg += body.charAt(ci) + body.charAt(ci + 1); ci++;
  } else if (body.charAt(ci) === delim) {
    segments.push(seg); seg = '';
  } else {
    seg += body.charAt(ci);
  }
}
segments.push(seg);
var pat = segments[0];
var repl = segments[1];
var flags = (segments[2] || '');

这使得 sed 表达式 s#/(\d+)/(\d+)/(\d+)#2-3-$1#g 能够正确解析为三段:模式、替换和标志。

4.2 awk 渲染端本地模拟

awk 语法复杂,完整实现需要引入 awkjs 等解析器,会增加包体积和启动延迟。本方案在渲染进程中用 cut 模拟实现 print 表达式解析,覆盖 80% 的字段提取场景:

js 复制代码
// renderer.js 中的 awk 模拟器:用 cut 逻辑解析 print 表达式
function buildAwkInput(input) {
  var expr = (input.awkExpr || input.expression || '').trim();
  var delim = input.delimiter || '\t';
  if (delim === '\\t' || !delim) delim = /\s+/;
  var text = input.text || '';
  if (!text.trim()) throw new Error('请输入文本');

  // 解析 print 表达式
  var printMatch = /^print\s+(.+)$/i.exec(expr);
  if (!printMatch) throw new Error('仅支持 print 表达式(如 print $1, $3)');

  var fieldRefs = printMatch[1].split(',').map(function(s) { return s.trim(); });
  var indices = [];
  for (var fi = 0; fi < fieldRefs.length; fi++) {
    var numMatch = /\$(\d+)/.exec(fieldRefs[fi]);
    if (numMatch) indices.push(parseInt(numMatch[1]) - 1);
    else indices.push(-1);
  }

  var lines = text.split('\n');
  var output = [];
  for (var li = 0; li < lines.length; li++) {
    var cols = typeof delim === 'string' ? lines[li].split(delim) : lines[li].split(delim);
    var selected = [];
    for (var ii = 0; ii < indices.length; ii++) {
      if (indices[ii] >= 0 && indices[ii] < cols.length) selected.push(cols[indices[ii]].trim());
      else selected.push('');
    }
    output.push(selected.join(' '));
  }
  return { _rawOutput: output.join('\n'), _stats: '提取 ' + fieldRefs.join(', ') + '(' + lines.length + ' 行)' };
}

4.3 面板拖拽分隔条

输入和输出面板之间的分隔条支持鼠标拖拽,动态调整两个面板的高度比例:

js 复制代码
function initResizeHandle() {
  var handle = el('panelResize');
  if (!handle) return;
  var dragging = false;
  var startY = 0, inputStart = 1, outputStart = 1.2;
  var inputPanel, outputPanel;

  handle.addEventListener('mousedown', function(e) {
    e.preventDefault();
    dragging = true;
    startY = e.clientY;
    handle.classList.add('dragging');
    inputPanel = handle.previousElementSibling;
    outputPanel = handle.nextElementSibling;
    inputStart = parseFloat(getComputedStyle(inputPanel).flexGrow) || 1;
    outputStart = parseFloat(getComputedStyle(outputPanel).flexGrow) || 1.2;
    document.body.style.cursor = 'row-resize';
    document.body.style.userSelect = 'none';
  });

  document.addEventListener('mousemove', function(e) {
    if (!dragging) return;
    var totalHeight = handle.parentElement.clientHeight;
    var dy = e.clientY - startY;
    var dFlex = (dy / totalHeight) * (inputStart + outputStart);
    var newIn = Math.max(0.3, inputStart + dFlex);
    var newOut = Math.max(0.3, inputStart + outputStart - newIn);
    inputPanel.style.flex = newIn;
    outputPanel.style.flex = newOut;
  });

  document.addEventListener('mouseup', function() {
    if (!dragging) return;
    dragging = false;
    handle.classList.remove('dragging');
    document.body.style.cursor = '';
    document.body.style.userSelect = '';
  });
}

4.4 grep 多文件输出行号高亮

grep 在多文件模式下输出格式为 filename:linenum:line,需要区分文件名和行号分别着色。使用三段正则解析:

js 复制代码
var m = /^(.+?):(\d+):(.*)$/.exec(lines[i]);
if (m) {
  // 多文件模式:文件名用 peach 色,行号用 yellow 色
  html += '<span class="grep-filename">' + escapeHtml(m[1]) + '</span>:<span class="grep-linenum">'
    + escapeHtml(m[2]) + ':</span>' + escapeHtml(m[3]) + '\n';
} else {
  // 单文件模式:只高亮行号
  var colonIdx = lines[i].indexOf(':');
  if (colonIdx >= 0 && /^\d+$/.test(lines[i].substring(0, colonIdx))) {
    html += '<span class="grep-linenum">' + escapeHtml(lines[i].substring(0, colonIdx + 1)) + '</span>'
      + escapeHtml(lines[i].substring(colonIdx + 1)) + '\n';
  } else {
    html += escapeHtml(lines[i]) + '\n';
  }
}

4.5 剪贴板复制兜底机制

输出内容复制功能采用 navigator.clipboard API 优先、execCommand 兜底的双通道策略:

js 复制代码
function copyOutput() {
  var pre = el('outputContent').querySelector('.output-pre');
  var text = pre ? pre.textContent : '';
  if (!text) { notify('提示', '没有可复制的输出内容', ''); return; }
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(text).then(function() {
      notify('已复制', '输出内容已复制到剪贴板', 'success');
      var btn = el('btnCopy');
      btn.textContent = '✓ 已复制'; btn.classList.add('copied');
      setTimeout(function() { btn.textContent = '📋 复制'; btn.classList.remove('copied'); }, 2000);
    });
  } else {
    // 兜底:创建隐藏 textarea + execCommand
    var ta = document.createElement('textarea');
    ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px';
    document.body.appendChild(ta); ta.select();
    document.execCommand('copy'); ta.remove();
    notify('已复制', '输出内容已复制到剪贴板', 'success');
  }
}

4.6 预置示例设计

每个工具预置两组可直接执行的示例,确保用户打开应用即可体验真实功能:

工具 示例 1 示例 2
grep Server 日志搜索(ERROR/WARN 匹配) HTTP 状态码过滤(500/404/401/403)
sed 邮箱脱敏(alice@company.com → @ 日期格式转换(2026/08/27 → 08-27-2026)
awk 员工表字段提取(姓名+部门+薪资) 进程列表提取(用户+CPU+命令)
cut TSV 成绩表(提取 Name/Score/Grade 列) 系统 /etc/passwd(提取用户+描述+Shell)
tr 大小写转换(全句大写化) 删除标点符号(清理电话号码和邮箱)
wc 英文段落统计(5 行 pangram) 鸿蒙介绍(6 行中文段落)
sort 字母排序(7 种水果) 成绩排名(10 个分数,数值排序)
uniq 水果去重(12 行 → 6 行) 日志级别计数(INFO/WARN/ERROR 统计)
head 前 5 行提取(鸿蒙系统文档) 前 3 行提取(希腊字母序列)
tail 后 5 行提取(部署 10 步骤) 后 3 行提取(星期序列)
diff JavaScript 代码变更对比 配置文件差异对比
cat 带行号显示 多文件连接
tee 日志行号标记 安装步骤编号

五、深色主题设计

5.1 Catppuccin Mocha 配色方案

采用 18 个 CSS 变量构建 Catppuccin Mocha 深色主题,确保在鸿蒙 PC 设备上具有良好的可读性:

js 复制代码
:root {
  --base: #1e1e2e;          /* 主背景 */
  --mantle: #181825;         /* 侧栏背景 */
  --crust: #11111b;          /* 最深层背景 */
  --surface0: #313244;       /* 输入框背景 */
  --surface1: #45475a;       /* 边框 */
  --surface2: #585b70;       /* 悬浮态 */
  --text: #cdd6f4;           /* 主文字 */
  --text-muted: #a6adc8;     /* 次要文字 */
  --overlay0: #6c7086;       /* 占位符文字 */
  --blue: #89b4fa;           /* 链接/按钮 */
  --green: #a6e3a1;          /* 成功/diff 添加 */
  --red: #f38ba8;            /* 错误/diff 删除 */
  --yellow: #f9e2af;         /* 行号高亮 */
  --mauve: #cba6f7;          /* 工具徽章 */
  --peach: #fab387;          /* 文件名高亮 */
  --teal: #94e2d5;           /* 状态栏 */
  --border: #313244;         /* 默认边框 */
  --border-light: #45475a;   /* 浅边框 */
}

5.2 交互增强

侧栏工具项增加了滑动悬停效果和渐变头部:

js 复制代码
.sidebar-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 18px 14px;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, rgba(203, 166, 247, 0.06) 0%, transparent 100%);
}
.tool-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  cursor: pointer;
  border-left: 3px solid transparent;
  transition: background 0.15s, border-color 0.2s, padding-left 0.15s;
}
.tool-item:hover {
  padding-left: 16px;
  background: var(--surface0);
}
.tool-item.active {
  background: rgba(137, 180, 250, 0.08);
  border-left-color: var(--blue);
}
.tool-item.active .tool-name { color: #b4d0fb; }

diff 输出使用绿色添加行和红色删除行配合左侧彩色边线:

js 复制代码
.diff-add {
  color: var(--green);
  background: rgba(166, 227, 161, 0.08);
  display: block;
  padding: 0 4px 0 14px;
  border-left: 2px solid var(--green);
  margin: 0 -16px;
}
.diff-del {
  color: var(--red);
  background: rgba(243, 139, 168, 0.08);
  display: block;
  padding: 0 4px 0 14px;
  border-left: 2px solid var(--red);
  margin: 0 -16px;
}

六、构建与部署

6.1 同步到 web_engine 部署目录

js 复制代码
# 复制全部应用文件(含 tools.js 工具引擎)
$appDir = "web_engine\src\main\resources\resfile\resources\app"
Copy-Item "electron-apps\GnuWin32\main.js" $appDir -Force
Copy-Item "electron-apps\GnuWin32\tools.js" $appDir -Force
Copy-Item "electron-apps\GnuWin32\renderer.js" $appDir -Force
Copy-Item "electron-apps\GnuWin32\index.html" $appDir -Force
Copy-Item "electron-apps\GnuWin32\package.json" $appDir -Force
Copy-Item "electron-apps\GnuWin32\styles\gnuwin32.css" "$appDir\styles\" -Force

注意:tools.js 是工具引擎核心文件(13 款工具实现),务必确认它进入了部署目录,否则主进程 require('./tools') 会直接失败。

6.2 构建与真机运行

  1. DevEco Studio 打开工程 → Build → Build Hap(s)/APP(s) → Build Hap(s)
  2. 连接鸿蒙 PC → Run → Run(需配置 signingConfigs)

6.3 真机功能验证

验证步骤 操作 预期结果
1 打开应用,观察侧栏工具列表 14 款工具按三个分类显示,grep 默认选中并加载示例
2 点击「执行」运行 grep 示例 输出面板显示匹配行,exec-stats 显示执行耗时
3 切换 sed 工具,执行邮箱脱敏示例 邮箱地址被替换为 @,原始邮箱不可见
4 拖拽输入/输出面板之间的分隔条 两个面板比例自由调整,拖拽过程流畅无闪烁
5 清空 grep 输入框的 pattern 后执行 自定义 div 通知弹出错误提示(非原生 alert)
6 点击输出面板的「复制」按钮 通知提示"已复制",剪贴板内容正确

其中第 2、3 步是整个"真实适配"的核心验证点:工具执行结果来自真实 Node.js 处理,不是预置的静态输出。

七、常见问题与解决方案

Q1:原生对话框导致应用崩溃

问题现象:在鸿蒙 PC 真机上运行应用,执行工具报错时调用 alert 弹出错误提示,应用立即崩溃白屏。

根本原因:鸿蒙 Electron 适配层对 alert/confirm/prompt 的底层实现会创建 SubWindow,SubWindow 创建过程存在已知缺陷导致页面崩溃。

错误做法:

js 复制代码
// ❌ 直接使用原生对话框------鸿蒙 SubWindow 崩溃
alert('执行失败: ' + error.message);
var confirmed = confirm('确定要清空输出吗?');

解决方案:使用纯 DOM 构建自定义通知组件,替代所有原生对话框:

js 复制代码
// ✅ 自定义通知组件(renderer.js notify 函数)
function notify(title, msg, type) {
  var old = el('notification');
  if (old) old.remove();
  var div = document.createElement('div');
  div.id = 'notification';
  div.className = 'gw-notification' + (type === 'error' ? ' error' : type === 'success' ? ' success' : '');
  div.innerHTML = '<div class="notification-title">' + escapeHtml(title) + '</div>' +
    '<div class="notification-msg">' + escapeHtml(msg) + '</div>';
  document.body.appendChild(div);
  setTimeout(function() { if (div.parentNode) div.remove(); }, 4000);
}

通知支持三种类型:默认(蓝色)、error(红色)、success(绿色),4 秒后自动消失。

Q2:原生 select 元素触发 SubWindow 崩溃

问题现象:工具选项面板中如果包含原生 select 下拉框,即使不主动点击,渲染过程中也会触发 SubWindow 创建导致崩溃。

根本原因:鸿蒙 Electron 适配层对原生 select 元素的下拉面板实现同样依赖 SubWindow,与 alert/confirm 属于同一类已知限制。

错误做法:

js 复制代码
<!-- ❌ 使用原生 select 元素------SubWindow 崩溃 -->
<select id="optType">
  <option value="all">全部</option>
  <option value="f">文件</option>
  <option value="d">目录</option>
</select>

解决方案:所有选项均使用 checkbox 和 text input 构建,完全规避原生 select:

js 复制代码
// ✅ renderOptions:checkbox + text input 替代 select(renderer.js)
function renderOptions(options) {
  var bar = el('optionsBar');
  if (!options || options.length === 0) { bar.innerHTML = '<div class="opt-hint">此工具无额外选项,直接在输入区输入文本</div>'; return; }
  var html = '';
  for (var i = 0; i < options.length; i++) {
    var o = options[i];
    if (o.type === 'check') {
      html += '<div class="opt-group"><label class="check-label"><input type="checkbox" id="' + o.id + '"' +
        (o.default ? ' checked' : '') + '> ' + escapeHtml(o.label) + '</label></div>';
    } else {
      html += '<div class="opt-group"><label class="opt-label">' + escapeHtml(o.label) + '</label>' +
        '<input type="text" class="opt-input" id="' + o.id + '" placeholder="' + escapeHtml(o.placeholder || '') + '"' +
        (o.default !== undefined ? ' value="' + escapeHtml(String(o.default)) + '"' : '') + '></div>';
    }
  }
  bar.innerHTML = html;
}

工具选项面板中全部使用 checkbox(如 -i 忽略大小写、-n 显示行号、-r 逆序)和 text input(如分隔符、行数、搜索路径),既保证了交互灵活性,又完全规避了 SubWindow 崩溃风险。

Q3:grep 多文件搜索时后续文件匹配数异常偏少

问题现象:grep 搜索多个文件中的 ERROR 模式,第一个文件匹配结果正确,但后续文件匹配数异常偏少甚至为空。

根本原因:全局正则对象(flags 含 g)的 lastIndex 属性会在多次 test 调用之间保持状态。第一个文件搜索结束后 lastIndex 停留在末尾位置,下一个文件的首行从该位置开始匹配,大量行被跳过。

错误做法:

js 复制代码
// ❌ 全局正则不重置 lastIndex------多文件搜索结果丢失
var re = new RegExp(pattern, 'g');
for (var si = 0; si < sources.length; si++) {
  var lines = sources[si].content.split('\n');
  for (var li = 0; li < lines.length; li++) {
    if (re.test(lines[li])) { output.push(lines[li]); }
    // lastIndex 未重置,下一个文件从上一文件末尾位置继续匹配
  }
}

解决方案:每次 test 调用后立即重置 lastIndex 为 0:

js 复制代码
// ✅ 每次 test 后重置 lastIndex(tools.js grep 节选)
for (var li = 0; li < lines.length; li++) {
  var matched = re.test(lines[li]);
  re.lastIndex = 0;  // 关键:重置全局正则状态
  if (matched !== invertMatch) {
    matches++;
    totalMatches++;
    if (!countOnly) {
      var prefix = lineNumber ? (li + 1) + ':' : '';
      if (sources.length > 1) prefix = src.name + ':' + prefix;
      output.push(prefix + lines[li]);
    }
  }
}

Q4:sed 表达式含斜杠时解析错误

问题现象:用户输入 s//\d+/NUM/g 替换路径中的数字,解析器报错"无效正则"或匹配到错误内容。

根本原因:旧方案使用简单正则解析 s/pattern/replacement/flags 三段,非贪婪 .+? 在遇到第一个 / 时就认为段结束,无法区分转义的 / 与段分隔符 /。

错误做法:

js 复制代码
// ❌ 简单正则解析------无法处理转义斜杠
var m = /^s\/(.+?)\/(.*)\/([gi]*)$/.exec(expression);
// s/\/\d+/NUM/g 在第一个 \/ 处就被错误截断

解决方案:手动逐字符解析,遇到 \ 时将当前和下一个字符一起收入段缓冲区,同时支持 #、| 等任意分隔符:

js 复制代码
// ✅ 支持转义 + 任意分隔符(tools.js sed 节选)
var delim = expression.charAt(1);  // 自动检测分隔符(/、#、| 等)
var body = expression.substring(2);
var segments = []; var seg = '';
for (var ci = 0; ci < body.length; ci++) {
  if (body.charAt(ci) === '\\' && ci + 1 < body.length) {
    // 转义字符:将当前和下一个字符一起加入段
    seg += body.charAt(ci) + body.charAt(ci + 1); ci++;
  } else if (body.charAt(ci) === delim) {
    segments.push(seg); seg = '';
  } else {
    seg += body.charAt(ci);
  }
}
segments.push(seg);

这使得 s#/\d+/\d+/\d+#2-3-$1#g 和 s//\d+/NUM/g 都能正确解析为三段:模式、替换和标志。

Q5:diff 大文本对比时 UI 卡死

问题现象:粘贴两段各超过 10000 行的文本执行 diff 对比,应用界面无响应,数秒后可能报内存不足错误。

根本原因:diff 使用 O(n²) 动态规划算法计算最小编辑距离,10000×10000 的二维数组需要分配约 400MB 内存,超出 JavaScript 单线程堆限制。

错误做法:

js 复制代码
// ❌ 不限制行数------大文本 O(n²) 内存溢出
var linesA = textA.split('\n');  // 可能 10000+ 行
var linesB = textB.split('\n');  // 可能 10000+ 行
var dp = [];
for (var i = 0; i <= linesA.length; i++) {
  dp[i] = new Array(linesB.length + 1);  // 10000 × 10000 → ~400MB
}

解决方案:对比前限制最大行数(5000 行),超出部分截断并提示用户:

js 复制代码
// ✅ 限制最大行数防止内存溢出(tools.js diff 节选)
var linesA = textA.split('\n');
var linesB = textB.split('\n');
var maxLen = Math.max(linesA.length, linesB.length);
var n = Math.min(maxLen, 5000);

var dp = [];
for (var i = 0; i <= n; i++) {
  dp[i] = new Array(n + 1);
  dp[i][0] = i;
}

5000 行的上限覆盖了绝大多数实际使用场景(代码文件、配置文件、日志片段),同时保证内存占用在安全范围内。

八、总结

本文完整记录了 GnuWin32 Unix 工具集在鸿蒙 PC 平台的适配过程。核心技术要点总结如下:

技术点 方案
工具引擎 13 款工具使用 fs/path/os 内置模块,零外部依赖
IPC 通信 6 个 handler 覆盖工具执行、文件读写、目录浏览、系统对话框
sed 解析器 逐字符解析 + 转义处理 + 任意分隔符支持
awk 模拟 渲染进程 print 表达式解析,覆盖 80% 字段提取场景
界面布局 侧栏工具列表 + 输入/输出双面板 + 可拖拽分隔条
输出着色 grep 多文件输出文件名/行号分别着色 + diff 绿色添加/红色删除
预置示例 每个工具两组示例,加载后直接执行
鸿蒙安全 自定义 div 通知替代 alert/confirm/prompt,checkbox 替代 select
主题配色 Catppuccin Mocha(18 个 CSS 变量)

核心经验:适配过程中遇到了五类典型问题------原生对话框 SubWindow 崩溃(Q1)、原生 select SubWindow 崩溃(Q2)、grep 全局正则 lastIndex 状态污染(Q3)、sed 分隔符转义解析(Q4)、diff 大文本 O(n²) 内存溢出(Q5),每个问题都通过阅读源码定位根因并针对性修复。其中 Q1/Q2 属于鸿蒙平台已知约束的防御性规避,Q3/Q4/Q5 属于工具实现层面的工程问题。

整个适配过程遵循 Electron 壳方案标准化流程:在 electron-apps/GnuWin32/ 开发目录中编写代码,同步到 web_engine/ 模块部署目录,最终由鸿蒙壳工程打包为 HAP 安装包。开发者专注于 Node.js 工具逻辑与 Web 技术栈,无需关心平台差异。

相关推荐
wei_shuo11 小时前
鸿蒙平台 Haroopad Markdown 编辑器适配实战:基于 Electron 壳方案的实时预览写作工具
openharmony
wei_shuo12 小时前
鸿蒙平台 Cypress 前端测试框架适配实战:基于 Electron 壳方案的纯 JavaScript BDD 测试引擎开发
openharmony
wei_shuo13 小时前
鸿蒙平台 ES Agent REST API 测试工具适配实战:基于 Electron 壳方案的跨平台 HTTP 调试客户端开发
openharmony
wei_shuo14 小时前
鸿蒙平台 H2 SQL 工作台适配实战:基于 Electron 壳方案的 Navicat 风格数据库管理工具开发
openharmony
wei_shuo1 天前
Flutter 三方库 OpenHarmony 鸿蒙适配实战:in_app_update 应用内更新插件 ArkTS 原生适配全流程
openharmony·in_app_update
Java的搬运工3 天前
HarmonyOS ArkUI V2 实战:Schema 驱动表单、2in1 适配与实时通信
harmonyos·arkts·openharmony·harmonyos next·表单校验·arkui v2·2in1
熊猫钓鱼>_>4 天前
Flutter app_settings 鸿蒙适配实战:Intent 体系到 Want 的跨越
flutter·华为·harmonyos·openharmony·intent·want
哈__6 天前
Flutter 3.44.9 + OpenHarmony7:image_cropper三方库 图片裁剪的应用
flutter·openharmony
moyh-blog24 天前
OpenHarmony播放音乐start请求从media_service到audio_server的调用流程02
openharmony·audio