前言

在将 Abricotine 适配到鸿蒙 PC 平台时,文档重新加载功能遇到了一个严重的问题:用户点击"重新加载"(Reload)时,当前打开的文档内容会丢失,用户需要重新打开文件。经过深入排查,我们发现问题的根本原因是重新加载会清空窗口内容,但没有保存和恢复文档路径的机制。

本文将详细记录这个问题的完整解决方案,包括问题分析、路径保存机制设计、IPC 通信实现、CodeMirror 刷新策略等关键技术点,确保文档重新加载功能在鸿蒙 PC 上完美运行。

关键词:鸿蒙PC、Electron适配、文档重新加载、路径保存、IPC通信、CodeMirror、状态持久化
在这里插入图片描述

目录

  1. 问题现象与影响分析
  2. 根本原因深度分析
  3. 路径保存机制设计
  4. 完整实现方案
  5. IPC 通信实现
  6. CodeMirror 刷新策略
  7. 最佳实践与注意事项
  8. 常见问题解答
  9. 总结与展望

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

问题现象与影响分析

1.1 问题现象

在鸿蒙 PC 上重新加载文档时,出现以下问题:

错误现象

用户操作:点击"重新加载"(Reload)
结果:
  ├─ 窗口重新加载  ✅ 正常
  ├─ 文档内容丢失  ❌ 不应该发生
  └─ 用户需要重新打开文件  ❌ 用户体验差

控制台日志

[HarmonyOS Renderer] reload command called
[HarmonyOS Renderer] Current path: /path/to/document.md
[HarmonyOS Renderer] Window reloaded
[HarmonyOS Renderer] Document content: (empty)  ❌ 内容丢失

表现

  • ❌ 重新加载后文档内容为空
  • ❌ 用户需要手动重新打开文件
  • ❌ 未保存的更改可能丢失
  • ❌ 用户体验极差

1.2 问题影响

这个错误会导致:

  • 数据丢失风险:如果用户有未保存的更改,重新加载会导致更改丢失
  • 用户体验差:用户需要重新打开文件,操作繁琐
  • 工作流程中断:用户正在编辑文档时,重新加载会中断工作流程
  • 信任度下降:用户可能认为应用不稳定,不敢使用重新加载功能

1.3 触发场景

以下操作都会触发这个问题:

  1. 手动重新加载开发者 > 重新加载
  2. 快捷键重新加载Ctrl+RCmd+R
  3. 代码热更新:开发模式下代码更新后自动重新加载

根本原因深度分析

2.1 原始实现的问题

问题1:直接关闭文档

在原始代码中,reload 命令会直接关闭文档:

// 问题代码(错误示例)
reload: function(win, abrDoc, cm) {
    abrDoc.close(true); // ❌ 关闭文档,内容丢失
    win.webContents.reloadIgnoringCache();
}

原因分析

  • abrDoc.close(true) 会清空文档内容
  • 重新加载后,文档内容无法恢复
  • 没有保存文档路径的机制

问题2:缺少路径保存机制

原始代码没有保存文档路径的机制:

// 问题代码(错误示例)
reload: function(win, abrDoc, cm) {
    // 没有保存路径
    win.webContents.reloadIgnoringCache();
    // 重新加载后,无法知道之前打开的是哪个文件
}

原因分析

  • 重新加载会清空窗口状态
  • 文档路径信息丢失
  • 无法在重新加载后恢复文档

2.2 Electron 重新加载机制

Electron 重新加载流程

  1. 触发重新加载:调用 win.webContents.reloadIgnoringCache()
  2. 清空窗口内容:窗口内容被清空
  3. 重新加载 HTML:从 index.html 重新加载
  4. 重新初始化 JavaScript:所有 JavaScript 代码重新执行
  5. 状态丢失:之前的状态(包括文档路径)丢失

关键点

  • 重新加载会完全重置窗口状态
  • 需要在重新加载前保存状态
  • 需要在重新加载后恢复状态

2.3 IPC 通信的限制

问题3:IPC 通信是异步的

IPC 通信是异步的,无法在重新加载前同步获取路径:

// 问题代码(错误示例)
reload: function(win, abrDoc, cm) {
    var currentPath = abrDoc.path; // ✅ 可以获取路径
    // 但是如何保存到主进程?
    win.webContents.reloadIgnoringCache();
    // 重新加载后,如何恢复路径?
}

原因分析

  • 渲染进程的状态在重新加载后会丢失
  • 需要将状态保存到主进程
  • 主进程的状态在重新加载后仍然存在

路径保存机制设计

3.1 设计目标

路径保存机制需要实现以下目标:

  1. 保存文档路径:在重新加载前保存当前文档路径
  2. 恢复文档内容:在重新加载后恢复文档内容
  3. 自动恢复:用户无需手动操作
  4. 错误处理:如果恢复失败,提供友好的错误提示

3.2 核心设计思路

思路1:主进程保存路径

在主进程中保存文档路径:

// 主进程(abr-application.js)
setPathToLoad: function (path, winId) {
    var win = this.getFocusedAbrWindow(winId);
    if (win) {
        win.path = path; // 保存到窗口对象
    }
}

思路2:渲染进程获取路径

在重新加载后,从主进程获取保存的路径:

// 渲染进程(abr-document.js)
this.ipcClient.trigger("getPathToLoad", undefined, function (pathFromMain) {
    if (pathFromMain) {
        // 恢复文档
        that.open(pathFromMain);
    }
});

思路3:重新加载前保存

在重新加载前保存路径:

// 渲染进程(commands.js)
reload: function(win, abrDoc, cm) {
    var currentPath = abrDoc.path;
    if (currentPath) {
        abrDoc.ipcClient.trigger("setPathToLoad", currentPath);
    }
    win.webContents.reloadIgnoringCache();
}

完整实现方案

4.1 主进程实现

abr-application.js 中的实现

// trigger
setPathToLoad: function (path, winId) {
    // ⚠️ HarmonyOS: 保存路径,用于重新加载后恢复文档
    var win = this.getFocusedAbrWindow(winId);
    if (win) {
        win.path = path;
        console.log('[HarmonyOS Application] setPathToLoad: saved path for reload:', path);
    }
},

// trigger
getPathToLoad: function (arg, winId, callback) {
    var win = this.getFocusedAbrWindow(winId),
        path = win ? win.path : null;
    if (typeof callback === "function") {
        callback(path);
    } else {
        return path;
    }
}

关键点

  • setPathToLoad:保存文档路径到窗口对象
  • getPathToLoad:从窗口对象获取保存的路径
  • 使用窗口 ID 区分不同的窗口

4.2 渲染进程实现

commands.js 中的实现

reload: function(win, abrDoc, cm) {
    // ⚠️ HarmonyOS: 不关闭文档,直接重新加载页面
    // 原代码会关闭文档,导致用户丢失未保存的内容
    // abrDoc.close(true);
    var currentPath = abrDoc.path;
    if (currentPath) {
        abrDoc.ipcClient.trigger("setPathToLoad", currentPath);
    }
    win.webContents.reloadIgnoringCache();
}

关键点

  • 不调用 abrDoc.close(true),避免清空文档内容
  • 保存当前文档路径到主进程
  • 调用 reloadIgnoringCache() 重新加载

abr-document.js 中的实现

open: function (path) {
    var that = this;
    var isHarmonyOS = (typeof process !== 'undefined' && process.env && process.env.HARMONYOS === 'true') ||
                      (typeof window !== 'undefined' && window.__HARMONYOS__ === true);

    if (isHarmonyOS) {
        console.log('[HarmonyOS Renderer] isHarmonyOS detected: true');
        // HarmonyOS: 尝试从主进程获取路径,如果存在则打开
        // 否则,弹出对话框让用户选择文件
        this.ipcClient.trigger("getPathToLoad", undefined, function (pathFromMain) {
            console.log('[HarmonyOS Renderer] getPathToLoad callback called with path:', pathFromMain);
            var finalPath = path || pathFromMain;

            if (!finalPath) {
                console.log('[HarmonyOS Renderer] No path from main, asking user to open file.');
                finalPath = that.dialogs.askOpenPath();
            }

            if (!finalPath) {
                console.log('[HarmonyOS Renderer] User cancelled file open dialog.');
                return false;
            }

            console.log('[HarmonyOS Renderer] AbrDocument.open() called with path:', finalPath);

            // 如果当前文档是干净的且没有路径,或者用户强制打开新窗口,则在当前窗口打开
            if ((!that.path && that.isClean()) || (path && that.isClean())) {
                console.log('[HarmonyOS Renderer] Opening file in current window:', finalPath);
                files.readFile(finalPath, function (data, readPath) {
                    console.log('[HarmonyOS Renderer] open() readFile callback called, data type:', typeof data, 'data length:', data ? data.length : 0, 'path:', readPath);
                    if (data === null || data === undefined) {
                        console.error('[HarmonyOS Renderer] ⚠️ File read returned null/undefined in open()');
                        console.error('[HarmonyOS Renderer] ⚠️ File path was:', readPath);
                        var errorMsg = that.localizer.get('file-open-error') || '无法打开文件';
                        if (readPath) {
                            errorMsg = (that.localizer.get('file-open-error-with-path') || '无法打开文件: ${path}').replace('${path}', readPath);
                        }
                        that.dialogs.showErrorBox(
                            that.localizer.get('dialog-error') || '错误',
                            errorMsg
                        );
                        return;
                    }
                    console.log('[HarmonyOS Renderer] ✅ File content loaded, length:', data.length);
                    console.log('[HarmonyOS Renderer] ✅ File content preview (first 100 chars):', data.substring(0, Math.min(100, data.length)));
                    that.stopWatcher(); // 停止旧文件的监听
                    that.clear(data || "", readPath); // 设置新内容
                    that.startWatcher(); // 开始监听新文件
                    that.updateWindowTitle(); // 更新标题
                    // 通知主进程更新窗口路径
                    that.ipcClient.trigger("setWinPath", readPath);
                  
                    // ⚠️ 关键:强制刷新编辑器以确保内容显示
                    setTimeout(function() {
                        if (that.cm) {
                            console.log('[HarmonyOS Renderer] Force refreshing CodeMirror after file load');
                            that.cm.refresh();
                            that.cm.focus();
                            // 确保内容已设置
                            var currentValue = that.cm.doc.getValue();
                            console.log('[HarmonyOS Renderer] CodeMirror current value length after refresh:', currentValue.length);
                            if (currentValue.length === 0 && data && data.length > 0) {
                                console.warn('[HarmonyOS Renderer] ⚠️ Content lost, re-setting value');
                                that.cm.doc.setValue(data);
                                that.cm.refresh();
                            } else {
                                console.log('[HarmonyOS Renderer] ✅ Content successfully displayed in editor');
                            }
                        }
                    }, 100);
                });
                return that.ipcClient.trigger("setWinPath", finalPath);
            } else {
                console.log('[HarmonyOS Renderer] Opening file in new window:', finalPath);
                return that.openNewWindow(finalPath);
            }
        });
    } else {
        // 非 HarmonyOS 平台,使用原始逻辑
        // ...
    }
}

关键点

  • open() 函数中,首先尝试从主进程获取保存的路径
  • 如果存在保存的路径,自动打开该文件
  • 如果不存在,弹出文件选择对话框
  • 打开文件后,强制刷新 CodeMirror 编辑器

4.3 初始化时的路径恢复

abr-document.js 初始化时的实现

// 在文档初始化时,检查是否有保存的路径
this.ipcClient.trigger("getPathToLoad", undefined, function (pathFromMain) {
    if (pathFromMain) {
        console.log('[HarmonyOS Renderer] Found saved path on initialization:', pathFromMain);
        // 延迟打开,确保 DOM 已初始化
        setTimeout(function() {
            that.open(pathFromMain);
        }, 100);
    }
});

关键点

  • 在文档初始化时检查是否有保存的路径
  • 如果有,自动打开该文件
  • 延迟打开,确保 DOM 已初始化

IPC 通信实现

5.1 IPC 通道定义

主进程(ipc-server.js)中的定义

// setPathToLoad: 保存路径
ipcMain.on('setPathToLoad', function(event, path) {
    var winId = event.sender.id;
    abrApp.setPathToLoad(path, winId);
});

// getPathToLoad: 获取路径
ipcMain.on('getPathToLoad', function(event) {
    var winId = event.sender.id;
    abrApp.getPathToLoad(null, winId, function(path) {
        event.returnValue = path;
    });
});

关键点

  • setPathToLoad:同步 IPC,保存路径
  • getPathToLoad:同步 IPC,获取路径
  • 使用窗口 ID 区分不同的窗口

5.2 渲染进程调用

ipc-client.js 中的实现

trigger: function (command, args, callback) {
    // 发送 IPC 消息到主进程
    var result = ipcRenderer.sendSync(command, args);
    if (typeof callback === "function") {
        callback(result);
    }
    return result;
}

关键点

  • 使用 sendSync 同步发送 IPC 消息
  • 支持回调函数处理结果
  • 返回 IPC 结果

CodeMirror 刷新策略

6.1 问题分析

问题:重新加载后,CodeMirror 编辑器可能不显示内容

原因

  • CodeMirror 在 DOM 初始化后才创建
  • 文件内容可能在 CodeMirror 创建之前设置
  • 需要手动刷新 CodeMirror 才能显示内容

6.2 解决方案

延迟刷新策略

// ⚠️ 关键:强制刷新编辑器以确保内容显示
setTimeout(function() {
    if (that.cm) {
        console.log('[HarmonyOS Renderer] Force refreshing CodeMirror after file load');
        that.cm.refresh();
        that.cm.focus();
        // 确保内容已设置
        var currentValue = that.cm.doc.getValue();
        console.log('[HarmonyOS Renderer] CodeMirror current value length after refresh:', currentValue.length);
        if (currentValue.length === 0 && data && data.length > 0) {
            console.warn('[HarmonyOS Renderer] ⚠️ Content lost, re-setting value');
            that.cm.doc.setValue(data);
            that.cm.refresh();
        } else {
            console.log('[HarmonyOS Renderer] ✅ Content successfully displayed in editor');
        }
    }
}, 100);

关键点

  • 延迟 100ms 后刷新,确保 CodeMirror 已创建
  • 检查内容是否已设置
  • 如果内容丢失,重新设置内容
  • 调用 refresh() 强制刷新显示

6.3 多重保险机制

策略1:延迟刷新

setTimeout(function() {
    that.cm.refresh();
}, 100);

策略2:内容检查

var currentValue = that.cm.doc.getValue();
if (currentValue.length === 0 && data && data.length > 0) {
    that.cm.doc.setValue(data);
    that.cm.refresh();
}

策略3:焦点设置

that.cm.focus(); // 确保编辑器获得焦点

最佳实践与注意事项

7.1 路径保存机制

✅ 推荐做法

  1. 保存路径:在重新加载前保存当前文档路径
  2. 主进程保存:将路径保存到主进程,避免重新加载后丢失
  3. 自动恢复:在重新加载后自动恢复文档
  4. 错误处理:如果恢复失败,提供友好的错误提示

❌ 避免的做法

  1. 不保存路径:直接重新加载,不保存路径
  2. 渲染进程保存:在渲染进程保存路径(重新加载后会丢失)
  3. 手动恢复:要求用户手动重新打开文件
  4. 忽略错误:不处理恢复失败的情况

7.2 CodeMirror 刷新

✅ 推荐做法

  1. 延迟刷新:使用 setTimeout 延迟刷新,确保 CodeMirror 已创建
  2. 内容检查:检查内容是否已设置,如果丢失则重新设置
  3. 多重保险:使用多种策略确保内容显示
  4. 焦点设置:设置编辑器焦点,提升用户体验

❌ 避免的做法

  1. 立即刷新:不等待 CodeMirror 创建就刷新
  2. 不检查内容:不检查内容是否已设置
  3. 单一策略:只使用一种策略,不提供多重保险
  4. 忽略焦点:不设置编辑器焦点

7.3 IPC 通信

✅ 推荐做法

  1. 同步 IPC:使用同步 IPC 保存和获取路径
  2. 窗口 ID:使用窗口 ID 区分不同的窗口
  3. 错误处理:处理 IPC 通信失败的情况
  4. 日志记录:记录 IPC 通信日志,便于调试

❌ 避免的做法

  1. 异步 IPC:使用异步 IPC(可能导致时序问题)
  2. 不区分窗口:不区分不同的窗口
  3. 忽略错误:不处理 IPC 通信失败的情况
  4. 无日志记录:不记录 IPC 通信日志

常见问题解答

8.1 为什么需要保存路径到主进程?

问题:为什么不能在渲染进程保存路径?

回答

  • 重新加载会清空渲染进程的所有状态
  • 主进程的状态在重新加载后仍然存在
  • 只有保存到主进程,才能在重新加载后恢复

8.2 为什么需要延迟刷新 CodeMirror?

问题:为什么不能立即刷新 CodeMirror?

回答

  • CodeMirror 在 DOM 初始化后才创建
  • 文件内容可能在 CodeMirror 创建之前设置
  • 延迟刷新确保 CodeMirror 已创建

8.3 为什么需要多重保险机制?

问题:为什么不能只使用一种策略?

回答

  • 不同环境下 CodeMirror 的创建时机可能不同
  • 多重保险机制确保在各种情况下都能正常工作
  • 提高代码的健壮性和可靠性

8.4 如何处理恢复失败的情况?

问题:如果恢复失败,应该如何处理?

回答

  1. 错误提示:显示友好的错误提示
  2. 文件选择对话框:弹出文件选择对话框,让用户重新选择文件
  3. 日志记录:记录错误日志,便于调试

8.5 如何测试路径保存机制?

问题:如何确认路径保存机制正常工作?

回答

  1. 打开文件:打开一个文件
  2. 重新加载:点击"重新加载"
  3. 检查恢复:确认文件自动恢复
  4. 检查内容:确认文件内容正确显示

总结与展望

9.1 技术成果

通过本次适配实践,我们成功解决了文档重新加载功能在鸿蒙 PC 上的路径丢失问题:

  1. 路径保存机制:通过主进程保存文档路径,确保重新加载后可以恢复
  2. 自动恢复机制:在重新加载后自动恢复文档,无需用户手动操作
  3. CodeMirror 刷新策略:通过延迟刷新和多重保险,确保编辑器内容正确显示
  4. 错误处理完善:提供友好的错误提示,提升用户体验

9.2 关键技术点

  1. 主进程状态保存:将路径保存到主进程,避免重新加载后丢失
  2. IPC 同步通信:使用同步 IPC 保存和获取路径
  3. 延迟刷新策略:使用 setTimeout 延迟刷新 CodeMirror
  4. 多重保险机制:使用多种策略确保内容显示

9.3 适用场景

本方案适用于以下场景:

  1. 文档重新加载:重新加载当前打开的文档
  2. 代码热更新:开发模式下代码更新后自动重新加载
  3. 窗口恢复:窗口关闭后重新打开,恢复之前的文档

9.4 未来优化方向

  1. 未保存更改提示:在重新加载前提示用户保存未保存的更改
  2. 多文档支持:支持多个文档的路径保存和恢复
  3. 恢复状态扩展:不仅保存路径,还保存光标位置、滚动位置等状态

相关资源


Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐