鸿蒙PC:鸿蒙版本 Electron 框架环境搭建并且实现 XH 笔记应用
欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:[鸿蒙PC开发者社区]:https://harmonypc.csdn.net/
第一部分:鸿蒙版本 Electron 框架环境搭建
环境准备:
- 操作系统:macOS 26+
- IDE:DevEco Studio 5.0+
- Node.js:v18.x 或更高版本(建议 v20.18.1)
- 硬件:≥8GB 内存(推荐 16GB)、≥20GB 可用存储空间
- 目标设备:HarmonyOS(API 20+)PC 设备
这篇文章记录一次在 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 会自动同步工程。第一次打开可能会下载或索引一些依赖,等待同步完成即可。
工程里主要有两个模块:
| 模块 | 作用 |
|---|---|
electron | HAP 入口模块 |
web_engine | Web/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.vue | XH 笔记主页面,组合侧栏、编辑器、工具栏 |
NoteSidebar.vue | 搜索框和笔记列表 |
NoteEditor.vue | 标题、正文和编辑状态 |
NoteToolbar.vue | 新建、复制、导出、删除等操作 |
useNotes.ts | 笔记数据、搜索、选择、增删改、自动保存 |
useNativeBridge.ts | 复制、导出、通知等原生能力封装 |
useOhos.ts | 对window.ohos的基础封装 |
main.js | IPC 处理器,负责保存文件、通知、剪贴板等 |
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_KEY='xh-notes:v1'constreadNotes=():NoteItem[]=>{constraw=window.localStorage.getItem(STORAGE_KEY)if(!raw){returnseedNotes}constparsed=JSON.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.value=falselastSavedAt.value=Date.now()}constscheduleSave=()=>{isSaving.value=truewindow.clearTimeout(saveTimer)saveTimer=window.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 class="title-input" :value="note.title" placeholder="笔记标题" @input="$emit('updateTitle', ($event.target as HTMLInputElement).value)" />正文输入:
<textarea class="content-input" :value="note.content" placeholder="开始记录..." spellcheck="false" @input="$emit('updateContent', ($event.target as HTMLTextAreaElement).value)" ></textarea>编辑器不直接修改笔记对象,而是通过事件把输入内容交给外层页面,再由useNotes.ts执行更新。这种写法比在组件里直接改对象更清晰,后续要加撤销、历史记录或云同步时也更好处理。
底部统计信息包含:
- 字符数量
- 段落数量
- 更新时间
- 保存状态
统计逻辑:
constcontentStats=computed(()=>{consttext=props.note.content.trim()constchars=props.note.content.lengthconstparagraphs=text?text.split(/\n\s*\n/).length:0return`${chars}字符 /${paragraphs}段`})九、实现顶部工具栏
顶部工具栏组件是:
src/components/NoteToolbar.vue它负责展示当前运行环境和常用操作按钮。
工具栏上有一个运行环境提示:
鸿蒙运行时 浏览器预览这个状态来自桥接层:
constisNativeRuntime=computed(()=>isOhosEnv)如果当前页面运行在普通浏览器中,就显示“浏览器预览”;如果运行在鸿蒙版本 Electron 环境中,并且window.ohos.isOhos为真,就显示“鸿蒙运行时”。
按钮包括:
- 新建
- 复制正文
- 导出 Markdown
- 删除
当没有选中笔记时,复制、导出和删除按钮会禁用,避免空状态下误操作。
十、封装原生桥接能力
XH 笔记没有在页面里直接访问window.ohos,而是新增了一层封装:
src/composables/useNativeBridge.ts它对外暴露:
const{isNativeRuntime,copyText,exportMarkdown,notify,setWindowTitle,}=useNativeBridge()页面层只关心“复制正文”“导出 Markdown”“通知用户”,不需要知道底层是通过浏览器 API 还是通过鸿蒙版本 Electron 的桥接 API 完成。
复制正文:
constcopyText=async(text:string)=>{if(!text.trim()){returnfalse}returnawaitclipboard.write(text)}导出 Markdown:
constexportMarkdown=async(title:string,content:string)=>{constfileName=`${safeFileName(title)}.md`if(!isOhosEnv){downloadInBrowser(fileName,content)return{ok:true}}constfilePath=awaitsaveFile({title:'导出 Markdown',defaultPath:fileName,filters:[{name:'Markdown',extensions:['md']}],})if(!filePath){return{ok:false,canceled:true}}constok=awaitwriteTextFile(filePath,content)return{ok,filePath}}这里做了一个重要的降级:如果当前是普通浏览器预览,就使用浏览器下载能力导出 Markdown;如果当前是鸿蒙运行时,就调用保存对话框和写文件能力。
这样开发体验会好很多。写 UI 时可以先在浏览器里快速调试,等逻辑稳定后再跑到鸿蒙环境验证原生能力。
十一、给运行时补充写文件能力
原有桥接层已经有保存文件对话框,但导出 Markdown 不只是拿到路径,还需要把内容写进去。所以这次在运行时层补了一条 IPC。
在main.js中新增:
constfs=require('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 封装:
constwriteTextFile=async(filePath:string,content:string):Promise<boolean>=>{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.vue'importNoteSidebarfrom'@/components/NoteSidebar.vue'importNoteToolbarfrom'@/components/NoteToolbar.vue'import{useNativeBridge}from'@/composables/useNativeBridge'import{useNotes}from'@/composables/useNotes'笔记状态来自useNotes:
const{filteredNotes,currentNote,currentNoteId,searchTerm,isSaving,lastSavedLabel,createNote,selectNote,updateNote,deleteNote,togglePinned,}=useNotes()原生能力来自useNativeBridge:
const{copyText,exportMarkdown,notify,isNativeRuntime}=useNativeBridge()复制当前笔记:
consthandleCopyNote=async()=>{if(!currentNote.value){return}constok=awaitcopyText(currentNote.value.content)if(ok){showFeedback('正文已复制')awaitnotify('XH 笔记','正文已复制到剪贴板')}else{showFeedback('复制失败,请检查权限')}}导出当前笔记:
consthandleExportNote=async()=>{if(!currentNote.value){return}constresult=awaitexportMarkdown(currentNote.value.title,formatMarkdown())if(result.ok){showFeedback(result.filePath?`已导出${result.filePath}`:'Markdown 已导出')awaitnotify('XH 笔记','Markdown 导出成功')}elseif(!result.canceled){showFeedback('导出失败,请稍后重试')}}这里还有一个小细节:导出前会把标题和正文拼成 Markdown:
constformatMarkdown=()=>{consttitle=currentNote.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 技术栈的路径。
