鸿蒙PC:鸿蒙版本 Electron 框架环境搭建并且实现 XH 笔记应用

发布时间:2026/5/22 6:57:35

鸿蒙PC:鸿蒙版本 Electron 框架环境搭建并且实现 XH 笔记应用 欢迎加入鸿蒙PC开发者社区共同打造开发者工具生态[鸿蒙PC开发者社区]https://harmonypc.csdn.net/第一部分鸿蒙版本 Electron 框架环境搭建环境准备操作系统macOS 26IDEDevEco Studio 5.0Node.jsv18.x 或更高版本建议 v20.18.1硬件≥8GB 内存推荐 16GB、≥20GB 可用存储空间目标设备HarmonyOSAPI 20PC 设备这篇文章记录一次在 Mac 上搭建鸿蒙版 Electron 开发环境的过程。这里说的“鸿蒙版 Electron”不是在 macOS 上直接跑普通 Electron 桌面应用而是使用 Electron for HarmonyOS 预编译包v34.6.0-20251105.1-release这类工程把前端 html页面、Electron 风格的主进程和 preload 桥接逻辑放进 HarmonyOS/OpenHarmony 应用工程里运行。第一步 找到仓库访问 Electron 鸿蒙仓库华为云官方开源electron开源仓库下载最新 Release 包如 v34.6.0-20251105.1-release.zip。第二步 下载到本地并且将解压此时你看到的libelectron这个文件夹就是解压缩之后得到的预编译的官方开源的项目了。第三步 预览libelectron内部目录结构可以看到跟根目录中有两个文件夹lib.unstripped文件夹里面存放的是两个so库资源。两一个文件夹ohos_hap下面则是我们熟悉的鸿蒙项目的正常的目录结构了。将ohos_hap在DevEco Studio中打开。注意不是打开libelectron根目录而是打开ohos_hap。因为ohos_hap才是 DevEco Studio 能识别的 HarmonyOS/OpenHarmony 工程目录。打开后DevEco Studio 会自动同步工程。第一次打开可能会下载或索引一些依赖等待同步完成即可。工程里主要有两个模块模块作用electronHAP 入口模块web_engineWeb/Electron 运行时和桥接层build-profile.json5里可以看到当前工程的 SDK 配置和签名配置例如{compatibleSdkVersion:5.0.5(17),runtimeOS:HarmonyOS,targetSdkVersion:5.0.5(17)}第四步 预览最终显示的文件注意electron鸿蒙版本实际上还是将web页面塞到鸿蒙原生应用的形式。所以最终运行的是静态文件夹下的前端页面。第五步 确认签名配置鸿蒙应用运行到真机或模拟器时需要签名配置。这个项目的ohos_hap/build-profile.json5中已经有signingConfigs{name:default,type:HarmonyOS,material:{certpath:/Users/luqingjiedemac/.ohos/config/default_ohos_hap_xxx.cer,keyAlias:debugKey,profile:/Users/luqingjiedemac/.ohos/config/default_ohos_hap_xxx.p7b,storeFile:/Users/luqingjiedemac/.ohos/config/default_ohos_hap_xxx.p12}}这里文章里不要直接暴露自己的完整证书文件名和密码。写博客时可以像上面这样用xxx隐去敏感部分。如果你本地没有这些签名文件可以在 DevEco Studio 里重新生成调试签名。一般路径是File - Project Structure - Signing Configs或者在运行配置中根据 DevEco Studio 的提示自动生成 Debug 签名。常见签名问题包括profile文件不存在certpath文件不存在storeFile文件不存在bundleName和 profile 不匹配使用了别人的本地绝对路径如果项目是从别人电脑拷贝过来的最容易遇到第 5 个问题。解决方式是不要硬改证书密码而是在 DevEco Studio 里重新生成自己的 Debug 签名配置。第六步 连接设备或启动模拟器项目要真正证明跑通最好运行到鸿蒙设备或模拟器上。如果使用真机需要打开开发者模式和 USB 调试。连接后用hdc查看设备/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc list targets如果能看到设备编号说明设备连接成功。如果使用模拟器可以在 DevEco Studio 的 Device Manager 中创建或启动模拟器。模拟器启动后同样可以用hdc list targets确认。第七步 将华为云开源的项目运行起来第二部分实现 XH 笔记应用第二部分不再停留在“能不能启动工程”而是直接做一个小应用XH 笔记。这个案例的目标不是做一个功能复杂的商业笔记软件而是用一个足够完整的小应用验证鸿蒙版本 Electron 框架在真实业务场景里的开发方式。页面层使用 Vue3运行时保留 Electron 风格的 preload 和 IPC 桥接最终由鸿蒙工程负责打包和运行。此案例开源地址 https://AtomGit.com/lqjmac/XHbj/tree/main最终 XH 笔记包含这些能力功能说明笔记列表左侧展示所有笔记支持置顶和更新时间排序搜索笔记根据标题和正文筛选笔记新建笔记点击按钮创建一条新笔记编辑笔记支持标题和正文编辑自动保存输入后自动写入本地存储删除笔记删除当前选中的笔记复制正文调用桥接能力写入剪贴板导出 Markdown通过保存对话框导出.md文件系统通知复制或导出成功后发送通知鸿蒙环境运行在鸿蒙设备或模拟器中验证运行效果一、为什么选择做 XH 笔记环境搭建文章通常只能证明“工程能打开、命令能跑”。但真正做应用时还会遇到状态管理、页面结构、文件能力、剪贴板、通知、开发模式和生产构建之间的差异。XH 笔记刚好适合作为第一个实战案例。它的业务足够简单不会被复杂需求带偏同时它又不是纯展示页必须处理真实交互用户输入标题和正文应用需要保存数据列表需要跟随更新时间刷新搜索需要即时过滤复制和导出需要调用运行时能力最终还要跑到鸿蒙环境里验证这几个点串起来就能完整验证 Vue3 页面、桥接层和鸿蒙应用容器之间的协作关系。二、整体实现思路XH 笔记采用左右分栏布局。左侧是笔记列表区域包含搜索框、新建按钮、笔记数量和笔记条目。每条笔记展示标题、正文摘要、更新时间如果被置顶还会显示置顶标识。右侧是编辑区域包含标题输入框、正文输入框、置顶按钮和底部状态栏。底部状态栏显示字符数量、段落数量、更新时间和自动保存状态。顶部是工具栏提供新建笔记复制正文导出 Markdown删除笔记当前运行环境提示自动保存时间提示这样设计的原因是笔记应用的高频操作都在一个页面内完成不需要跳转多个页面也更适合桌面和平板类窗口。三、页面文件和模块拆分这次主要改动 Vue3 业务层和运行时桥接层。Vue3 侧新增和修改的文件如下src/views/Home.vue src/components/NoteSidebar.vue src/components/NoteEditor.vue src/components/NoteToolbar.vue src/composables/useNotes.ts src/composables/useNativeBridge.ts src/composables/useOhos.ts src/styles/global.css src/App.vue src/router/index.ts运行时桥接层新增了写文件能力main.js preload.js几个核心文件的职责如下文件职责Home.vueXH 笔记主页面组合侧栏、编辑器、工具栏NoteSidebar.vue搜索框和笔记列表NoteEditor.vue标题、正文和编辑状态NoteToolbar.vue新建、复制、导出、删除等操作useNotes.ts笔记数据、搜索、选择、增删改、自动保存useNativeBridge.ts复制、导出、通知等原生能力封装useOhos.ts对window.ohos的基础封装main.jsIPC 处理器负责保存文件、通知、剪贴板等preload.js安全暴露桥接 API 给 Vue3 页面这个拆分方式的重点是页面组件不直接操作底层原生 API也不直接到处读写localStorage。业务状态集中在useNotes.ts原生能力集中在useNativeBridge.ts。四、设计笔记数据结构一条笔记的数据结构并不复杂exportinterfaceNoteItem{id:stringtitle:stringcontent:stringcreatedAt:numberupdatedAt:numberpinned?:boolean}字段含义如下字段说明id笔记唯一标识title笔记标题content笔记正文createdAt创建时间updatedAt最近更新时间pinned是否置顶这里没有一开始就接数据库也没有直接把所有笔记做成文件。第一版先用localStorage做本地持久化是为了尽快把业务闭环跑通。这样做有两个好处。第一开发阶段可以在浏览器里快速预览不依赖鸿蒙设备和文件系统。第二等页面交互稳定之后再通过“导出 Markdown”接入文件能力这样更容易定位问题。如果一开始就把编辑、保存、文件读写、设备权限全部混在一起排查成本会高很多。五、实现笔记状态管理笔记状态集中放在src/composables/useNotes.ts这个文件负责初始化默认笔记从localStorage恢复历史笔记新建笔记选择笔记更新标题和正文删除笔记置顶笔记搜索过滤自动保存初始化时应用会先尝试读取本地缓存constSTORAGE_KEYxh-notes:v1constreadNotes():NoteItem[]{constrawwindow.localStorage.getItem(STORAGE_KEY)if(!raw){returnseedNotes}constparsedJSON.parse(raw)if(!Array.isArray(parsed)){returnseedNotes}returnparsed}实际代码里还做了字段校验避免本地缓存格式异常时页面直接崩掉。笔记列表排序也放在状态层处理constsortNotes(items:NoteItem[]){return[...items].sort((a,b){if(a.pinned!b.pinned){returna.pinned?-1:1}returnb.updatedAt-a.updatedAt})}这样组件层拿到的列表已经是排好序的组件只负责渲染不关心排序规则。六、实现自动保存自动保存是这个案例里很关键的一步。它让 XH 笔记从“表单演示”变成了一个真正可以持续使用的小应用。实现思路是标题或正文变化时更新当前笔记的updatedAt然后延迟写入localStorage。核心逻辑如下letsaveTimer:number|undefinedconstpersistNow(){window.localStorage.setItem(STORAGE_KEY,JSON.stringify(notes.value))isSaving.valuefalselastSavedAt.valueDate.now()}constscheduleSave(){isSaving.valuetruewindow.clearTimeout(saveTimer)saveTimerwindow.setTimeout(persistNow,300)}这里没有每输入一个字符就立即保存而是做了 300ms 的延迟。用户连续输入时保存动作会被合并体验上仍然是自动保存但减少了频繁写入。页面上会显示保存状态正在保存 已保存 22:15:08这个状态在顶部工具栏和编辑器底部都会出现。这样用户能明确知道内容已经落盘不需要再找“保存按钮”。七、实现左侧笔记列表左侧列表组件是src/components/NoteSidebar.vue它接收三个核心参数defineProps{notes:NoteItem[]currentNoteId:stringsearchTerm:string}()它向外抛出三个事件defineEmits{select:[id:string]create:[]update:searchTerm:[value:string]}()这样侧栏组件本身不保存业务状态。搜索词、当前选中笔记、新建动作都交给父组件和useNotes.ts处理。笔记摘要也在组件里做了简单处理constgetExcerpt(note:NoteItem){returnnote.content.trim().replace(/\s/g, ).slice(0,56)||暂无正文}展示时一条笔记会包含标题是否置顶正文摘要更新时间搜索时状态层会同时匹配标题和正文returnsortedNotes.value.filter((note){return(note.title.toLowerCase().includes(keyword)||note.content.toLowerCase().includes(keyword))})八、实现右侧编辑器右侧编辑器组件是src/components/NoteEditor.vue它负责标题输入、正文输入、置顶按钮和底部统计信息。标题输入input classtitle-input :valuenote.title placeholder笔记标题 input$emit(updateTitle, ($event.target as HTMLInputElement).value) /正文输入textarea classcontent-input :valuenote.content placeholder开始记录... spellcheckfalse input$emit(updateContent, ($event.target as HTMLTextAreaElement).value) /textarea编辑器不直接修改笔记对象而是通过事件把输入内容交给外层页面再由useNotes.ts执行更新。这种写法比在组件里直接改对象更清晰后续要加撤销、历史记录或云同步时也更好处理。底部统计信息包含字符数量段落数量更新时间保存状态统计逻辑constcontentStatscomputed((){consttextprops.note.content.trim()constcharsprops.note.content.lengthconstparagraphstext?text.split(/\n\s*\n/).length:0return${chars}字符 /${paragraphs}段})九、实现顶部工具栏顶部工具栏组件是src/components/NoteToolbar.vue它负责展示当前运行环境和常用操作按钮。工具栏上有一个运行环境提示鸿蒙运行时 浏览器预览这个状态来自桥接层constisNativeRuntimecomputed(()isOhosEnv)如果当前页面运行在普通浏览器中就显示“浏览器预览”如果运行在鸿蒙版本 Electron 环境中并且window.ohos.isOhos为真就显示“鸿蒙运行时”。按钮包括新建复制正文导出 Markdown删除当没有选中笔记时复制、导出和删除按钮会禁用避免空状态下误操作。十、封装原生桥接能力XH 笔记没有在页面里直接访问window.ohos而是新增了一层封装src/composables/useNativeBridge.ts它对外暴露const{isNativeRuntime,copyText,exportMarkdown,notify,setWindowTitle,}useNativeBridge()页面层只关心“复制正文”“导出 Markdown”“通知用户”不需要知道底层是通过浏览器 API 还是通过鸿蒙版本 Electron 的桥接 API 完成。复制正文constcopyTextasync(text:string){if(!text.trim()){returnfalse}returnawaitclipboard.write(text)}导出 MarkdownconstexportMarkdownasync(title:string,content:string){constfileName${safeFileName(title)}.mdif(!isOhosEnv){downloadInBrowser(fileName,content)return{ok:true}}constfilePathawaitsaveFile({title:导出 Markdown,defaultPath:fileName,filters:[{name:Markdown,extensions:[md]}],})if(!filePath){return{ok:false,canceled:true}}constokawaitwriteTextFile(filePath,content)return{ok,filePath}}这里做了一个重要的降级如果当前是普通浏览器预览就使用浏览器下载能力导出 Markdown如果当前是鸿蒙运行时就调用保存对话框和写文件能力。这样开发体验会好很多。写 UI 时可以先在浏览器里快速调试等逻辑稳定后再跑到鸿蒙环境验证原生能力。十一、给运行时补充写文件能力原有桥接层已经有保存文件对话框但导出 Markdown 不只是拿到路径还需要把内容写进去。所以这次在运行时层补了一条 IPC。在main.js中新增constfsrequire(fs)ipcMain.handle(ohos:writeTextFile,async(event,{filePath,content}){if(!filePath){returnfalse}awaitfs.promises.writeFile(filePath,content,utf8)returntrue})在preload.js中暴露file:{showSaveDialog:(options{})ipcRenderer.invoke(ohos:showSaveDialog,options),writeTextFile:(filePath,content)ipcRenderer.invoke(ohos:writeTextFile,{filePath,content}),}然后在useOhos.ts里增加 TypeScript 封装constwriteTextFileasync(filePath:string,content:string):Promiseboolean{if(!ohos)returnfalsetry{returnawaitohos.file.writeTextFile(filePath,content)}catch(e){console.error(写入文件失败:,e)returnfalse}}这样 Vue3 页面调用exportMarkdown时最终会走到运行时的ohos:writeTextFile完成真正的文件写入。十二、主页面如何串起所有能力主页面文件是src/views/Home.vue它把状态管理、桥接能力和 UI 组件组合起来。页面顶部引入importNoteEditorfrom/components/NoteEditor.vueimportNoteSidebarfrom/components/NoteSidebar.vueimportNoteToolbarfrom/components/NoteToolbar.vueimport{useNativeBridge}from/composables/useNativeBridgeimport{useNotes}from/composables/useNotes笔记状态来自useNotesconst{filteredNotes,currentNote,currentNoteId,searchTerm,isSaving,lastSavedLabel,createNote,selectNote,updateNote,deleteNote,togglePinned,}useNotes()原生能力来自useNativeBridgeconst{copyText,exportMarkdown,notify,isNativeRuntime}useNativeBridge()复制当前笔记consthandleCopyNoteasync(){if(!currentNote.value){return}constokawaitcopyText(currentNote.value.content)if(ok){showFeedback(正文已复制)awaitnotify(XH 笔记,正文已复制到剪贴板)}else{showFeedback(复制失败请检查权限)}}导出当前笔记consthandleExportNoteasync(){if(!currentNote.value){return}constresultawaitexportMarkdown(currentNote.value.title,formatMarkdown())if(result.ok){showFeedback(result.filePath?已导出${result.filePath}:Markdown 已导出)awaitnotify(XH 笔记,Markdown 导出成功)}elseif(!result.canceled){showFeedback(导出失败请稍后重试)}}这里还有一个小细节导出前会把标题和正文拼成 MarkdownconstformatMarkdown(){consttitlecurrentNote.value.title.trim()||未命名笔记return#${title}\n\n${currentNote.value.content.trim()}\n}这样用户导出的文件不是简单纯文本而是可以直接被 Markdown 编辑器识别的文档。十三、运行开发服务开发阶段先启动 Vue3/Vite 服务cdohos_hap/web_engine/src/main/resources/resfile/resources/app/vue-appnpmrun dev正常情况下会看到VITE v5.4.21 ready in xxx ms Local: http://127.0.0.1:5173/我本地启动后访问http://127.0.0.1:5173/可以正常打开 XH 笔记页面。如果端口被占用因为当前 Vite 配置使用了strictPort: true服务不会自动切换到其他端口而是直接报错。这样做的好处是鸿蒙运行时加载地址更稳定不会出现 Vite 跑到 5174 但应用仍然访问 5173 的情况。十四、构建前端产物开发服务验证后再构建生产产物npmrun build本次构建通过输出类似vite v5.4.21 building for production... ✓ 47 modules transformed. ../dist/index.html ../dist/assets/index-xxxx.css ../dist/assets/Home-xxxx.css ../dist/assets/index-xxxx.js ../dist/assets/Home-xxxx.js ../dist/assets/vue-xxxx.js ✓ built in xxxms这里的输出目录是ohos_hap/web_engine/src/main/resources/resfile/resources/app/dist也就是说Vue3 应用构建完成后会被放到运行时能够加载的位置。开发模式下可以加载 Vite 服务生产模式下则加载打包后的dist/index.html。十五、在 DevEco Studio 中运行前端构建完成后打开 DevEco Studio。注意这里要打开鸿蒙工程目录而不是外层目录。打开后确认模块和运行配置正常然后选择设备或模拟器运行。开发模式下可以保持 Vite 服务运行让应用加载http://localhost:5173这样修改 Vue3 页面后可以快速看到变化。生产模式下则先执行npmrun build再通过 DevEco Studio 构建和运行 HAP让应用加载打包后的静态资源。运行成功后设备或模拟器中应该能看到 XH 笔记界面包括左侧笔记列表、右侧编辑区和顶部工具栏。十六、验证复制和导出能力XH 笔记的实战价值不只在页面编辑还在于它调用了运行时能力。复制正文点击“复制正文”后页面会调用awaitcopyText(currentNote.value.content)在浏览器预览时它会走navigator.clipboard.writeText在鸿蒙版本 Electron 运行时它会走桥接层的剪贴板能力。成功后会出现正文已复制并尝试发送系统通知XH 笔记正文已复制到剪贴板导出 Markdown点击“导出 Markdown”后会把当前笔记转换成# 笔记标题 笔记正文然后调用保存对话框让用户选择保存位置。确认后由运行时写入文件。十七、开发中需要注意的几个点1. 不要让页面直接依赖原生对象如果 Vue 组件里到处写window.ohos.file.showSaveDialog()后面会很难维护。浏览器预览、鸿蒙运行时、异常降级都要在各个组件里重复判断。这次把原生能力集中封装在useNativeBridge.ts页面只调用业务语义明确的方法copyText()exportMarkdown()notify()这样组件更干净也方便后续替换底层实现。2. 自动保存不要写得太急笔记应用里用户输入非常频繁。如果每次input都立即写入存储虽然第一版也能跑但不是一个好的习惯。这次使用 300ms 延迟保存既保留自动保存体验又避免频繁写入。3. 浏览器预览和鸿蒙运行时要同时考虑这个案例保留了浏览器降级逻辑剪贴板可以走浏览器 Clipboard API导出 Markdown 可以走浏览器下载通知可以走浏览器 Notification API这样做的好处是前端页面开发不必每次都启动设备。等交互稳定后再进入 DevEco Studio 和鸿蒙运行环境验证桥接能力。4. 导出文件要分成两步保存 Markdown 不是一个动作而是两个动作通过保存对话框拿到用户选择的路径将 Markdown 内容写入这个路径所以桥接层需要同时具备showSaveDialog writeTextFile只有保存对话框是不够的。5. 运行环境状态要显示出来工具栏里显示“浏览器预览”或“鸿蒙运行时”看起来只是一个小状态但它在调试时很有用。当复制、导出、通知表现和预期不一致时先看当前运行环境就能快速判断是浏览器降级逻辑的问题还是鸿蒙桥接层的问题。十八、可以继续扩展的方向XH 笔记目前已经完成了一个基础闭环。后面可以继续扩展方向说明Markdown 预览增加编辑/预览双栏标签系统给笔记添加标签并按标签筛选文件导入从.md文件导入笔记文件夹分类按工作、学习、生活分类快捷键支持新建、搜索、导出等快捷键数据加密本地保存前加密正文云同步接入账号体系和远端同步原生菜单增加更像桌面应用的菜单操作如果继续往下做我会优先加 Markdown 预览和文件导入。因为这两个能力能继续验证 Web 页面和原生文件能力之间的配合。十九、总结通过 XH 笔记这个案例可以看到鸿蒙版本 Electron 框架不只是能展示一个 Vue 页面它可以承载一个有真实交互的小应用。这次实战主要验证了几件事Vue3 可以负责主要页面和业务状态localStorage可以先承担轻量本地持久化preload 和 IPC 可以把剪贴板、通知、文件能力暴露给前端浏览器预览和鸿蒙运行时可以共用一套业务页面前端构建产物可以被鸿蒙应用容器加载应用最终可以运行到鸿蒙设备或模拟器中这篇文章的重点不是“笔记应用本身有多复杂”而是把一个小应用从界面、状态、自动保存、原生能力到鸿蒙运行验证完整串起来。对于熟悉 Vue3 和 Electron 开发方式的人来说这种开发模型比较自然对于鸿蒙应用开发来说它也提供了一条复用 Web 技术栈的路径。

相关新闻