🐸 青蛙编程网
首页 / 前端 / Electron / 8. 综合练习:迷你记事本

8. 综合练习:迷你记事本

这是什么:本章把前 7 章全部串起来,做一个真的能用的记事本:打开文件 → 编辑 → 另存为,最后打包成 exe。

为什么用:单一知识点学会了不等于会做软件——记事本五脏俱全:窗口、界面、IPC、对话框、文件读写一样不缺,是检验学习的最好标尺。

如何用:5 个文件照抄 + 逐个理解,跑通后再自己加功能。

8.1 项目结构

新建一个干净项目(或复用原项目清空旧界面),最终结构:

mini-notepad/
├── package.json   ← 第 2 章的配置,main 指向 main.js
├── main.js        ← 窗口 + 两个 IPC 频道
├── preload.js     ← 暴露 open / save 两个 API
├── index.html     ← 界面
├── style.css      ← 样式
└── renderer.js    ← 界面交互

8.2 主进程:窗口与文件读写

main.js(核心就是两个频道):

const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const fs = require('fs')
const path = require('path')

function createWindow() {
  const win = new BrowserWindow({
    width: 900,
    height: 650,
    title: '迷你记事本',
    webPreferences: { preload: path.join(__dirname, 'preload.js') },
  })
  win.loadFile('index.html')
}

app.whenReady().then(() => {
  // 打开:弹对话框选文件 → 读出内容一起返回
  ipcMain.handle('note-open', async () => {
    const result = await dialog.showOpenDialog({
      title: '打开文件',
      properties: ['openFile'],
      filters: [{ name: '文本文件', extensions: ['txt', 'md'] }],
    })
    if (result.canceled) return null
    const filePath = result.filePaths[0]
    return { path: filePath, content: fs.readFileSync(filePath, 'utf-8') }
  })

  // 保存:弹对话框要路径 → 写入内容 → 返回保存位置
  ipcMain.handle('note-save', async (_event, content) => {
    const result = await dialog.showSaveDialog({
      title: '保存文件',
      defaultPath: '未命名.txt',
      filters: [{ name: '文本文件', extensions: ['txt'] }],
    })
    if (result.canceled) return null
    fs.writeFileSync(result.filePath, content, 'utf-8')
    return result.filePath
  })

  createWindow()
})

app.on('window-all-closed', () => {
  app.quit()
})
代码 作用
showOpenDialog + readFileSync 选路径(第 6 章)+ 读文件(Node 的 fs),打包成一个"打开"动作
showSaveDialog 弹"另存为"框,defaultPath 是默认文件名
writeFileSync(path, content, 'utf-8') 把界面传来的文本写进硬盘
返回 null 两条约定:取消 = null,界面据此不更新状态

8.3 preload:只暴露两个动作

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('noteAPI', {
  open: () => ipcRenderer.invoke('note-open'),
  save: (content) => ipcRenderer.invoke('note-save', content),
})

界面能做的只有"打开"和"保存"——没暴露的能力一概没有,这就是白名单的美感。

8.4 界面三件套

index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>迷你记事本</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <strong>📒 迷你记事本</strong>
    <button id="btn-open">打开</button>
    <button id="btn-save">保存</button>
    <span id="status">未打开文件</span>
  </header>
  <textarea id="editor" placeholder="在这里写点什么…"></textarea>
  <script src="renderer.js"></script>
</body>
</html>

style.css:

* { margin: 0; box-sizing: border-box; }

body {
  height: 100vh;
  display: flex;
  flex-direction: column;
  font-family: system-ui, sans-serif;
}

header {
  display: flex;
  gap: 8px;
  align-items: center;
  padding: 8px 12px;
  background: #1f2937;
  color: #fff;
}

header strong { margin-right: auto; }

button {
  padding: 4px 14px;
  cursor: pointer;
}

#status {
  font-size: 12px;
  opacity: 0.7;
}

#editor {
  flex: 1;
  resize: none;
  border: none;
  padding: 16px;
  font-size: 15px;
  line-height: 1.6;
  outline: none;
}

renderer.js:

const editor = document.getElementById('editor')
const status = document.getElementById('status')

document.getElementById('btn-open').addEventListener('click', async () => {
  const note = await window.noteAPI.open()
  if (!note) return
  editor.value = note.content
  status.textContent = note.path
})

document.getElementById('btn-save').addEventListener('click', async () => {
  const saved = await window.noteAPI.save(editor.value)
  if (!saved) return
  status.textContent = '已保存到 ' + saved
})

npm start,你会看到:

深色工具栏 + 大片编辑区的窗口。
点「打开」选一个 txt → 内容出现在编辑区、状态栏显示路径;
改几个字点「保存」→ 弹另存为框 → 状态栏提示"已保存到 …"。
用系统记事本打开刚存的文件,内容一字不差。

一个能日常应急的文本编辑器,从头到尾不到 100 行。

8.5 打包成品

把第 7 章的 build 配置抄进 package.json(productName 改成"迷你记事本"),然后:

npm run dist

release/ 里的 迷你记事本 1.0.0.exe 就是可分发的成品。

8.6 进阶方向(自己动手)

  1. 标题联动:打开/保存后把 win.setTitle(文件名)——主进程加频道或用 webContents 推送
  2. 快捷键:界面监听 Ctrl+S 触发保存(渲染进程 keydown 就行)
  3. 拖拽打开:textarea 监听 drop 事件拿 file.path,主进程按路径读文件
  4. 自动更新:正式产品接 electron-updater,发布新版后用户端自动升级
  5. 前端框架:界面复杂了换 Vue / React 工程化开发,打包前多一步构建,electron-builder 照常收尾

💡 动手改

  1. 给 filters 加 json 和 log 扩展名,试着打开一个 JSON 文件
  2. 加一个「新建」按钮:清空编辑区、状态栏复位为"未打开文件"
  3. 保存前检测内容没变就 return,跳过弹框(提示:记下上次保存的内容做对比)

📦 本章小结

  • 完整桌面软件 = 窗口(第 2 章)+ 进程分工(第 3 章)+ 网页界面(第 4 章)+ IPC(第 5 章)+ 原生能力(第 6 章)+ 打包(第 7 章)
  • 记事本的核心数据流:界面文本 → noteAPI.save → 主进程 fs 写盘
  • 取消操作返回 null 是贯穿始终的小约定
  • 恭喜毕业 🐸——前端板块的网页技术加上 Electron,你已经能把作品带出浏览器了

学完 Electron,回到 前端板块总览 看看还能点亮什么!

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。