一次信创项目适配里,我遇到一个非常典型的问题:一套基于 UEditor 的富文本编辑器,被包成了 Electron 桌面应用,部署到预装统信 UOS 的办公电脑上。安装流程很顺利,安装包也完整走完了,但用户双击桌面图标,应用没有任何反应。终端手动拉起来,只看到一行刺眼的错误:
error while loading shared libraries: libnss3.so: cannot open shared object file这绝对不是个例。Electron 应用在国产操作系统上跑不起来、装不上、白屏、中文输入法失效,几乎是每个做信创桌面应用的人都会撞上的墙。这篇文章不聊宏观趋势,只讲我怎么从“双击没反应”一路排查到“在国产系统里稳定运行”,包括 UEditor 这种老牌编辑器要怎么塞进 Electron 壳子、electron-builder 打包要填哪些信创专属配置、以及到了现场最容易翻车的几个坑。适合正打算把 Web 富文本能力搬到国产终端、或者已经在信创项目里被 Electron 折腾的人参考。
1. 信创环境不是“同一个Linux”:先摸清目标和对手
很多人第一次做国产系统适配,下意识把它当成“在 Linux 上跑起来”来处理。这个想法的确不全面。信创终端的环境差异,比普通 Linux 发行版之间的差异还要大,CPU 架构、内核版本、glibc 版本、包管理器、桌面协议,每一项都可能让同一个安装包在一个系统上顺利运行、在另一个系统上直接闪退。
1.1 一张表看清 CPU 与系统组合
先看最常见的 CPU 体系。Electron 应用的运行依赖系统里对应的二进制版本,CPU 架构不对,安装包根本装不上,或者装上就启动不了。
| CPU 体系 | 典型 CPU | Electron 官方二进制 | 常见系统场景 |
|---|---|---|---|
| x86_64 | Intel、AMD、兆芯 | 官方支持,问题最少 | 统信 UOS x86 版、麒麟 x86 版 |
| arm64 | 飞腾、鲲鹏 | 官方支持 | 飞腾整机、鲲鹏服务器上跑统信或麒麟 |
| loongarch64 | 龙芯 3A5000/3A6000 | 官方列表里没有,基本靠厂商提供构建 | 统信龙芯版、麒麟龙芯版 |
| sw64 | 申威 | 官方不支持 | 申威平台,适配成本和前两者不同 |
你在终端执行uname -m,看到的x86_64、aarch64、loongarch64,基本上就决定了你应该去打哪个架构的包。x86 的机器跑 arm64 的 Electron,装包那一步就开始报架构错;反过来,arm64 的机器跑 x86 包,即便用兼容层勉强装上,性能和稳定性也都不行。
实测下来,一个项目如果只打算覆盖最常见场景,就先把 x86_64 和 arm64 这两套包做扎实。龙芯这类平台,首先要确认设备厂商能不能提供对应架构的 Electron 运行时,提前在技术选型阶段确认好,比到了项目交付期再想办法靠谱得多。
1.2 glibc 是隐形门禁
如果说 CPU 架构是明面上的门槛,glibc 就是一道隐形门禁。Electron 的官方 Linux 二进制,对系统的 glibc 版本有明确要求。越新的 Electron,要求越高。
我项目里核对过,Electron 28 之后的官方包,普遍要求系统 glibc 在较新的版本,老系统上直接跑不起来;Electron 22 这类相对宽松的版本,在比较旧的信创系统上反而稳。统信 UOS 的不同版本、麒麟 V10 的不同更新批次之间,glibc 差异也不小,不能拿“都是 UOS”当成同一个环境。
查 glibc 版本很简单:
getconf GNU_LIBC_VERSION ldd --version如果你打的包要同时兼容新旧两代国产系统,方案就两个:要么把 Electron 主版本往下压,牺牲一部分新特性换兼容;要么对不同系统出不同安装包,用启动脚本做版本判断。我做项目时倾向于后一种,因为 Electron 版本压得太低,Chromium 内核太老,UEditor 的某些现代 CSS 效果又会出问题,两头受气。
1.3 最容易被忽略的包管理器差异
同一个“麒麟”名字下,桌面版和服务器版可能一个用apt、一个用yum/dnf。统信 UOS 多数是基于 Debian 体系,习惯用deb包;但某些信创系统又只认rpm。
打包阶段最稳的做法,是把安装包格式和操作系统的包管理器对应起来:
- 能识别为
apt系的系统,给deb包。 - 能识别为
yum/dnf系的系统,给rpm包。 - 别把 AppImage 当主力分发格式。AppImage 依赖 FUSE,而很多国产系统默认不带 FUSE,用户拿过去直接就是“解压了但启动不了”。
这也是为什么我强烈建议,在项目最开始就确定“适配矩阵”:哪几个操作系统版本、哪几种 CPU 架构、装包格式是 deb 还是 rpm、glibc 门槛是多高。这张矩阵,后面打包、测试、现场排障全都要对着它来。
2. UEditor 进 Electron 前,先给老编辑器补三处“体检”
UEditor 是很有年代感的富文本编辑器。说句实话,它的代码结构和现在的前端工程化风格差别挺大,塞进 Electron 壳子里,不能直接“原封不动”就指望它工作。Electron 本身是 Chromium,UEditor 在浏览器里能跑,但在 Electron 里跑,会有几个此前没人碰过的特殊问题。
2.1 三大技术债逐个拆
UEditor 的三处硬伤,分别是 jQuery 依赖、iframe 承载结构、以及document.execCommand这套已经过时的编辑命令体系。
先说 jQuery。UEditor 底层依赖 jQuery 1.10 左右的老版本。普通 Web 项目里,页面加载顺序控制好就没事;但在 Electron 渲染进程里,只要模块加载时序一乱,UE这个全局对象就可能没被挂上,编辑器直接初始化失败。我的做法是写一个独立的ueditor-bootstrap.js,在 DOMContentLoaded 之前按顺序加载jquery.js、ueditor.config.js、ueditor.all.min.js,保证全局顺序可控。
再说 iframe。UEditor 默认在 iframe 里做编辑,这在 Electron 里会牵扯到安全策略。如果webSecurity设置得过严,iframe 里加载相对路径的资源会被拦下来。这一点,下面自定义协议的部分会具体说。
最后是document.execCommand。Chromium 到现在还在兼容它,给的是 deprecation 警告,UEditor 的大部分基础操作依然能用。但在国产系统某些老内核版本的 Chromium 上,一些格式清除、段落合并的场景确实会出现状态不一致,我遇到过“清除格式”按钮把整篇内容样式清掉的情况。最后的解决方案很朴素:在 UEditor 外层包一层内容校验逻辑,每次编辑器内容变化后做一次 HTML 结构整理,把style里不该出现的多余属性过滤掉。
2.2 离线场景下的 serverUrl 与上传功能
UEditor 原本是给 Web 项目用的,上传图片、抓取远程图片、Word 粘贴中转,全都依赖后端的serverUrl。到了桌面端,很多时候是纯离线环境,根本没有后端可连。
我建议直接把这个包袱卸载掉。桌面编辑器场景下,图片上传可以改成走本地文件对话框,选中图片后通过 Electron 的 preload 桥接层,把图片转成 base64 或本地文件路径再插入编辑器内容。这样至少不会出现“编辑器能用,一传图片就白屏”的尴尬。
| 原功能 | 依赖 | 桌面端处理方案 |
|---|---|---|
| 图片上传 | 后端 serverUrl | 本地文件对话框 + IPC 读文件,插入 base64 |
| Word 粘贴 | 后端过滤接口 | 关闭远程抓取,用 UEditor 内置本地粘贴过滤 |
| 远程图片抓取 | 后端代理 | 关闭该按钮,避免跨域请求 |
| 公式编辑 | MathType 插件 | 用 KaTeX/MathJax 替代,不用系统插件 |
| 涂鸦、音乐 | Flash/插件 | 直接隐藏相关按钮 |
UE.getEditor初始化时,把serverUrl设为空字符串,同时过滤掉工具栏里与上传、远程抓取相关的按钮项。纯文本编辑、表格、代码块这些核心能力保留即可,够用也不容易出问题。
2.3 用自定义协议替代 file://
UEditor 的静态资源如果直接用file://加载,会遇到一个很现实的问题:Electron 的安全策略会对本地文件访问做限制,UEditor 的 iframe 内部需要加载相对路径的样式、脚本时,经常出现资源加载失败。我在项目里就是从file://切换到自定义协议解决的。
主进程里注册一个app://协议,把本地 dist 目录映射成协议可访问的资源:
// main.js const { app, protocol, net } = require('electron'); const { pathToFileURL } = require('url'); const path = require('path'); app.whenReady().then(() => { protocol.handle('app', (request) => { const urlPath = request.url.replace(/^app:\/\//, ''); return net.fetch(pathToFileURL(path.join(__dirname, urlPath)).toString()); }); });BrowserWindow 加载页面的时候,用app://协议加载,而不是file://。UEditor 内部 iframe 里的相对路径,也会沿着这个协议解析。实测下来,图片、CSS、JS 的加载稳定多了,iframe 里的样式错乱问题也少了很多。
还有一个小细节:UEditor 的 config 里,可以设置一个iframeCssUrl,给编辑区域指定统一的基础样式。这个值在自定义协议下要写成app://...的完整路径,否则编辑区里的字体、行高在国产系统的默认字体环境下会显得很难看。
2.4 与前端框架共存时的小心机
如果你是在 Vue 或 React 项目里集成 UEditor,还有一类问题:UEditor 会默认扫描页面里所有script标签并尝试自动初始化编辑器,这跟前端框架的组件生命周期很容易打架。最直接的办法是在ueditor.config.js里把自动初始化关掉:
// ueditor.config.js window.UEDITOR_CONFIG = { autoClearinitialContent: true, initialFrameWidth: '100%', initialFrameHeight: 600, serverUrl: '', zIndex: 1000 };然后在组件里手动初始化:
const editor = UE.getEditor('editor-container', config);每次组件销毁时,记得editor.destroy()。不销毁的话,Electron 窗口关闭再打开,会残留多个编辑器实例,内存上涨很直观,用户体感就是“越用越卡”。
3. 打包环节:electron-builder 要填好的“信创配置”
UEditor 侧面的问题解决后,真正的重头戏是打包。很多团队在自己的开发机上打包跑得很欢,一到国产系统就出问题,问题往往就出在 electron-builder 配置里,没填信创环境要用的那几项。
3.1 Electron 版本与二进制镜像
先定 Electron 版本。信创项目不建议追新,建议选择一个稳定性经过验证的版本,并且明确该版本对 glibc 的要求。我在多个信创系统上实测,Electron 27 左右版本在统信 UOS 和麒麟 V10 上都有不错的表现,新老系统覆盖比较均衡。版本定下来之后,要配置国内可用的 Electron 二进制镜像,否则electron-builder下载 Electron 压缩包时经常卡在半路:
export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ export ELECTRON_CUSTOM_DIR={{ version }}这一条看起来不起眼,但能直接省掉“打包打了半天,最后下载失败”的烦恼。
3.2 一份可以直接抄的 build 配置
electron-builder 的配置文件里,Linux 相关的 target、依赖声明这两块,是信创适配的关键。
{ "appId": "com.example.ueditor-editor", "directories": { "output": "release/${version}" }, "files": [ "dist/**/*", "main.js", "preload.js", "ueditor/**/*" ], "linux": { "target": ["deb", "rpm"], "icon": "build/icon.png", "category": "Office;TextEditor;", "synopsis": "Desktop rich text editor based on UEditor", "executableName": "ueditor-editor" }, "deb": { "depends": [ "libnss3", "libasound2", "libgbm1", "libatk-bridge2.0-0", "libcups2", "libgtk-3-0" ] }, "rpm": { "depends": [ "libnss3", "libasound2", "libgbm1", "libatk-bridge2.0-0", "libcups2", "gtk3" ] }, "artifactName": "${name}-${version}-${arch}.${ext}" }depends这一串不是随手写的。Electron 在 Linux 上运行,需要libnss3(网络通信相关)、libasound2(音频相关)、libgbm1(GPU 图形缓冲相关)这些底层库。如果不在安装包里预先声明依赖,用户安装时不会报错,但启动时就会像文章开头那样,提示找不到libnss3.so。
声明依赖后,安装时系统会提示自动补装缺失的库,很多“装上了但启动不了”的工单,就是因为漏了这一句。
3.3 桌面菜单里的图标和启动器
deb/rpm 装完后,系统应用菜单里显示的图标和启动命令来自/usr/share/applications下的 desktop 文件。electron-builder 会生成一个 desktop 文件,但有两点经常出问题:
- Exec 路径。默认值有时候指向安装目录里的二进制名,如果你的
executableName和实际产物名不一致,桌面图标点了没反应。 - Icon 路径。desktop 文件里的 Icon 字段会指向安装目录下的图标文件,如果打包时没带上对应尺寸的 icon,应用菜单里就是灰块或系统默认图标。
我在项目里习惯用一套比较规范的 icon 集,至少包含 256x256 的 png,并在 build 配置里把linux.icon指定为这个 png。打包时确保Icon=指向实际存在文件,装完就能在菜单里看到正常图标,也容易和别的应用区分开。
3.4 多架构安装包怎么出
信创环境需要同时支持 x86 和 arm64,electron-builder 的配置里可以通过单独指定架构来出包:
electron-builder --linux deb --x64 electron-builder --linux deb --arm64纯前端项目、没有原生 Node 模块的情况下,交叉打包相对容易。如果用了原生模块,就尽量在对应架构的构建机或 CI 上去出包,避免交叉编译的麻烦。UEditor 本身是纯 JS,没有原生依赖,所以我项目里 x86 和 arm64 的包都是直接出的,省了不少事。
出包后的命名建议带上架构,别让用户自己猜。上面配置里的${arch}会自动把x64、arm64塞进文件名,分发、排障都很方便。
4. 装到国产系统上最容易翻车的三个现场
安装包打好了,不等于现场就能跑。真正部署到用户手里,最常见的问题有三个。我逐个说说现象、原因和解决方案。
4.1 GPU 白屏和闪退
现象:应用启动后窗口一闪而过,或者整个窗口白屏,刷新也没用;终端手动启动,能看到 GPU 进程相关的报错。
原因:国产终端很多用的是集成显卡,显卡驱动未必完善,Chromium 的 GPU 加速进程在部分驱动上会崩溃。Electron 默认会启用 GPU 加速,于是启动即崩。
解决方案分两步走。第一步,在应用主进程里主动关闭硬件加速:
const { app } = require('electron'); app.disableHardwareAcceleration(); app.commandLine.appendSwitch('use-gl', 'swiftshader');swiftshader是 Chromium 自带的软件渲染器,性能比硬件加速差一些,但对富文本编辑这种场景来说,感知不明显,稳定性却大大提升。
第二步,如果用户那里仍然白屏,用应急参数启动验证:
./ueditor-editor --disable-gpu --disable-gpu-compositing但注意,--disable-gpu只是应急手段,不建议在 final 版本里长挂。长期关掉 GPU 加速,滚动和动画会掉帧,用户会抱怨“不流畅”。
4.2 中文输入法不上屏
现象:编辑器能打开,但切换到中文输入法时,候选框显示不出来,或者候选框在屏幕角落,跟光标位置完全对不上。
原因:国产桌面系统上,中文输入法的标准做法是让应用支持ibus或fcitx协议。Electron 应用如果没有正确设置输入法相关的环境变量,Chromium 就识别不到输入法的输入上下文。
解决方法是把输入法环境变量打到一个启动脚本里,或者直接在打包后的 desktop 文件里预置:
#!/bin/bash export GTK_IM_MODULE=ibus export QT_IM_MODULE=ibus export XMODIFIERS=@im=ibus exec /opt/ueditor-editor/ueditor-editor "$@"注意,这个变量值必须和系统实际用的输入法框架一致。系统是 fcitx 的,就把ibus换成fcitx。我看到过不少项目把GTK_IM_MODULE=ibus写死,结果用户那边实际是 fcitx,问题依旧。稳妥起见,可以在启动脚本里做一个检测:
if dbus-send --session --dest=org.fcitx.Fcitx --type=method_call /org/fcitx/Fcitx org.fcitx.Fcitx.About 2>/dev/null; then export GTK_IM_MODULE=fcitx export QT_IM_MODULE=fcitx export XMODIFIERS=@im=fcitx else export GTK_IM_MODULE=ibus export QT_IM_MODULE=ibus export XMODIFIERS=@im=ibus fi这个小脚本,解决了我项目里至少三台输入法失效的返修单。
4.3 高分屏与中文字体
现象:在部分国产整机上,界面元素要么字体发虚、要么整体比例失控,编辑器内中文显示效果尤其差。
原因有两个。一是系统未正确设置 DPI 缩放,Chromium 拿到的缩放因子不对;二是系统里中文字体缺失或未优先使用中文字体族。
针对缩放,可以按设备实测设置一个合理的缩放系数:
app.commandLine.appendSwitch('high-dpi-support', '1'); app.commandLine.appendSwitch('force-device-scale-factor', '1.5');这个 1.5 不是一个通用值。有些设备是 1080p 屏、系统缩放 1.25,有些是 2K 屏、缩放 1.5,得逐一确认。写成一个可配置项放到配置文件里,比在代码里写死好得多。
字体方面,很多信创系统预装的是开源字体,UEditor 默认的字体族名称在系统里未必有对应字体。我在 UEditor 的iframeCssUrl样式里,统一把字体族改成:
body { font-family: "Noto Sans CJK SC", "Source Han Sans SC", "PingFang SC", sans-serif; }如果没有 Noto CJK 和思源,再留一版 fallback 到系统默认中文字体。同时,在部署文档里建议系统安装中文字体包,比如思源黑体、文泉驿微米黑,能明显改善编辑器的渲染效果。
4.4 依赖缺失快速自救
如果用户已经装好包、双击没反应,先用最简单的方法看日志:
cd /opt/ueditor-editor ldd ./ueditor-editor | grep "not found"输出里凡是not found的,就是系统里缺的库。比如文章开头的libnss3.so,就在这个列表里。知道缺什么之后,离线处理可以直接安装本地 deb 包:
dpkg -i ./offline-pkgs/*.deb线上环境有网就简单了:
apt-get install -y libnss3 libasound2 libgbm1 libatk-bridge2.0-0 libcups2也可以直接把库文件放到应用目录下,通过设置LD_LIBRARY_PATH让应用优先加载本地库。但这个方案有版本冲突风险,我建议只作为应急手段,还是优先补装系统库。
5. 一张适配自检表,和一套现场排查流程
现场问题千变万化,但排查路径是有迹可循的。我把项目里整理的一套排查链路和自检表分享出来,照着走,大多数问题半小时内能定位。
5.1 三分钟定位问题的排查链路
第一步,确认系统基础信息。一台陌生设备送过来,先跑三条命令:
cat /etc/os-release uname -m getconf GNU_LIBC_VERSION这三条分别回答了三个问题:这是什么发行版?什么架构?glibc 多新?拿到结果,就知道应该装哪个安装包。
第二步,启动应用看输出。桌面图标没反应时,直接去终端里跑二进制:
ELECTRON_ENABLE_LOGGING=1 /opt/ueditor-editor/ueditor-editor 2> /tmp/editor.logELECTRON_ENABLE_LOGGING=1能让 Chromium 的日志打到 stderr。等应用崩溃或白屏后,打开/tmp/editor.log看最后几十行,大概率能直接看到崩溃原因。
第三步,看动态库缺失:
ldd /opt/ueditor-editor/ueditor-editor | grep "not found"第四步,还定位不到就开远程调试。在启动参数里带一个调试端口,本地或内网部署时可以快速打开开发者工具:
/opt/ueditor-editor/ueditor-editor --remote-debugging-port=9222然后本机浏览器访问http://127.0.0.1:9222,直接看到渲染进程的 console、DOM 和网络请求,前端报错全部一览无遗。排查结束后,记得关掉这个端口,别开着调试端口交付。
5.2 把环境检测做成启动自检
与其等用户报障,不如把上面这套逻辑内置到应用里。我在主进程启动时,先跑一个环境检测模块,把系统 ID、CPU 架构、glibc 版本、缺失动态库情况都收集起来,写入一个日志文件。
// env-check.js const fs = require('fs'); const { execSync } = require('child_process'); function getOsRelease() { const content = fs.readFileSync('/etc/os-release', 'utf8'); const id = content.match(/^ID=(.*)$/m)?.[1]?.replace(/"/g, '') || 'unknown'; const versionId = content.match(/^VERSION_ID="?(.*?)"?$/m)?.[1] || 'unknown'; return { id, versionId }; } function getArch() { return process.arch; } function getGlibc() { try { return execSync('getconf GNU_LIBC_VERSION').toString().trim(); } catch (e) { return 'unknown'; } }然后把结果打印到日志里。如果用户报障,远程传一份日志回来,根本不用到场,就能判断是安装包选错、依赖缺失,还是系统环境问题。
5.3 自检表模板
这张表可以直接放进项目文档,适配测试、现场验收都能用。
| 检查步骤 | 命令或对象 | 通过标准 |
|---|---|---|
| 系统发行版 | cat /etc/os-release | 明确 ID 和版本号,如 uos 1060、kylin V10 |
| CPU 架构 | uname -m | 与安装包架构一致 |
| glibc 版本 | getconf GNU_LIBC_VERSION | 与 Electron 版本要求匹配 |
| 动态库依赖 | ldd ./ueditor-editor | 无not found输出 |
| 启动日志 | 终端运行查看 stderr | 无崩溃、无 GPU 报错 |
| 中文输入法 | 打开编辑器输入中文 | 候选框跟随光标,能正常上屏 |
| 界面缩放 | 打开设置页面查看图片 | 文字清晰,界面比例正常 |
| 编辑器核心功能 | 新建文档、插入表格、粘贴纯文本 | 无白屏、无样式错乱 |
当初做完第一版适配后,我就是拿着这张表,在统信 UOS x86、统信 UOS arm64(飞腾)、麒麟 V10 三个环境各跑了一遍。大部分问题在测试阶段提前暴露,真正到用户手里的返修率低了很多。
最后再说两句
这个项目做完之后,我有一个很深的体会:信创适配这件事,最麻烦的不是某个单一的技术难点,而是“环境种类多,现场反馈慢,问题又碎”。唯一能对冲这种不确定性的办法,就是把前期调研做足,把安装包的架构、依赖声明、启动脚本一次配好,再把环境检测工具内置到应用里。安装阶段多花十分钟配置,后续能省掉几十张现场工单。
最后分享一个小技巧:给安装包命名时,一定要把架构和系统信息带全。比如ueditor-editor-1.4.0-arm64-ubuntu.deb,用户拿到包就知道该不该装。我见过太多人下发一个不带架构的deb包,x86 机器装不上,arm 机器也装不上,两边互相甩锅。命名规范这件事,看着小,实际是信创项目交付里最容易踩、也最容易被忽略的坑。