最近一直在摸索Electron,网上有大牛使用Electron-vue做的一些应用,但由于本人非科班开发人员,学习起来总是云里雾里的,最终还是回归原始的Electron开发,再逐步拓展其他栈的知识。好了,接下来是最近临摹的一个Electron记事本,原文是简书的作者鳗驼螺写的一个教程————从零开始写一个记事本app,地址 https://www.jianshu.com/p/57d910008612/
由于作者写的时候是2017年,版本已经非常久远了,第一次写的时候愣是没跑起来,而且功能相对简单,想要作为平时使用还是有点欠缺的,因此本人对该记事本App做了一些优化,增加了文件拖放读取,字数统计和另存为功能,用起来也还是可以的,目前禁用了最大化和自由缩放窗体的功能,因为一些适配效果不理想,因此暂时放弃该功能,先来看下效果吧。
知识点整理
用到的知识点比较多,主要是
- main和renderer两个进程的通信
- electron 对话框 dialog 及 菜单 Menu 两个模块的使用
- nodejs的fs模块用于文本读写
- html5的文件拖拽
主进程代码 main.js
const {app, BrowserWindow, ipcMain, Menu} = require('electron');
const path = require('path');
// 主菜单模板
const menuTemplate = [
{
label: ' 文件 ',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: function() {
mainWindow.webContents.send('action', 'new')
}
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: function() {
mainWindow.webContents.send('action', 'open')
}
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: function() {
mainWindow.webContents.send('action', 'save')
}
},
{
label: '另存为... ',
accelerator: 'CmdOrCtrl+Shift+S',
click: function() {
mainWindow.webContents.send('action', 'save-as')
}
},
{
type: 'separator'
},
{
label: '退出',
click: function() {
mainWindow.webContents.send('action', 'exit')
}
}
]
},
{
label: ' 编辑 ',
submenu: [
{ label: '返回', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: '重做', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
{ type: 'separator' }, //分隔线
{ label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: '删除', accelerator: 'CmdOrCtrl+D', role: 'delete' },
{ type: 'separator' }, //分隔线
{ label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectall' }
]
},
{
label: ' 帮助 ',
submenu: [
{
label: '关于... ',
click: async () => {
const { shell } = require('electron');
await shell.openExternal('https://segmentfault.com/u/shaomeng');
}
}
]
}
];
// 主窗体
let mainWindow;
// 安全退出初始化
let safeExit = false;
// 构建主菜单
let menu = Menu.buildFromTemplate (menuTemplate);
Menu.setApplicationMenu (menu);
// 主窗体初始化
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 620,
resizable:false,
backgroundColor: '#e9e9e9',
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js')
}
});
// 加载页面内容
mainWindow.loadFile('index.html');
// 开发者工具
// mainWindow.webContents.openDevTools();
// 窗体生命周期 close 操作
mainWindow.on('close', (e) => {
if(!safeExit) {
e.preventDefault();
}
mainWindow.webContents.send('action', 'exit');
});
// 窗体生命周期 closed 操作
mainWindow.on('closed', function() {
mainWindow = null;
});
}
// 程序生命周期 ready
app.on('ready', createWindow);
// 程序生命周期 window-all-closed
app.on('window-all-closed', function() {
if (process.platform !== 'darwin') app.quit();
});
// 程序生命周期 activate
app.on('activate', function() {
if (mainWindow === null) createWindow();
});
// 接收退出命令
ipcMain.on('exit', function() {
safeExit = true;
app.quit();
});
渲染进程代码 renderer.js
const ipcRenderer = require('electron').ipcRenderer; // electron 通信模块
const remote = require('electron').remote; // electron 主进程与渲染进程通信模块
const Menu = remote.Menu; // electron renderer进程的菜单模块
const dialog = remote.dialog; // electron 对话框模块
// 初始化基本参数
let isSave = true; // 初始状态无需保存
let txtEditor = document.getElementById('txtEditor'); // 获取文本框对象
let currentFile = null; // 初始状态无文件路径
let isQuit = true; // 初始状态可正常退出
// 右键菜单模板
const contextMenuTemplate = [
{ label: '返回', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: '重做', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
{ type: 'separator' }, //分隔线
{ label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: '删除', accelerator: 'CmdOrCtrl+D', role: 'delete' },
{ type: 'separator' }, //分隔线
{ label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectall' }
];
// 构建右键菜单
const contextMenu = Menu.buildFromTemplate(contextMenuTemplate);
txtEditor.addEventListener('contextmenu', (e) => {
e.preventDefault();
contextMenu.popup(remote.getCurrentWindow());
});
// 检测编辑器是否有内容更新,统计字数
txtEditor.oninput = (e) => {
if (isSave) {
document.title += ' *';
}
isSave = false;
// 字数统计
wordsCount();
}
// 菜单操作
ipcRenderer.on('action', (event, arg) => {
switch(arg) {
case 'new': // 新建文档
askSaveNeed();
initDoc();
break;
case 'open': // 打开文档
askSaveNeed();
openFile();
wordsCount();
break;
case 'save': // 保存当前文档
saveCurrentDoc();
break;
case 'save-as': // 另存为当前文档
currentFile = null;
saveCurrentDoc();
break;
case 'exit': // 退出
askSaveNeed();
if(isQuit) { // 正常退出
ipcRenderer.sendSync('exit');
}
isQuit = true; // 复位正常退出
break;
}
});
// 初始化文档
function initDoc() {
currentFile = null;
txtEditor.value = '';
document.title = 'Notepad - Untitled';
isSave = true;
document.getElementById("txtNum").innerHTML = 0;
}
// 询问是否保存命令
function askSaveNeed() {
// 检测是否需要执行保存命令
if (isSave) {
return;
}
// 弹窗类型为 message
const options = {
type: 'question',
message: '请问是否保存当前文档?',
buttons: [ 'Yes', 'No', 'Cancel']
}
// 处理弹窗操作结果
const selection = dialog.showMessageBoxSync(remote.getCurrentWindow(), options);
// 按钮 yes no cansel 分别为 [0, 1, 2]
if (selection == 0) {
saveCurrentDoc();
} else if(selection == 1) {
console.log('Cancel and Quit!');
} else { // 点击 cancel 或者关闭弹窗则禁止退出操作
console.log('Cancel and Hold On!');
isQuit = false; // 阻止执行退出
}
}
// 保存文档,判断新文档or旧文档
function saveCurrentDoc() {
// 新文档则执行弹窗保存操作
if(!currentFile) {
const options = {
title: 'Save',
filters: [
{ name: 'Text Files', extensions: ['txt', 'js', 'html', 'md'] },
{ name: 'All Files', extensions: ['*'] }
]
}
const paths = dialog.showSaveDialogSync(remote.getCurrentWindow(), options);
if(paths) {
currentFile = paths;
}
}
// 旧文档直接执行保存操作
if(currentFile) {
const txtSave = txtEditor.value;
saveText(currentFile, txtSave);
isSave = true;
document.title = "Notepad - " + currentFile;
}
}
// 选择文档路径
function openFile() {
// 弹窗类型为openFile
const options = {
filters: [
{ name: 'Text Files', extensions: ['txt', 'js', 'html', 'md'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile']
}
// 处理弹窗结果
const file = dialog.showOpenDialogSync(remote.getCurrentWindow(), options);
if(file) {
currentFile = file[0];
const txtRead = readText(currentFile);
txtEditor.value = txtRead;
document.title = 'Notepad - ' + currentFile;
isSave = true;
}
}
// 执行保存的方法
function saveText( file, text ) {
const fs = require('fs');
fs.writeFileSync( file, text );
}
// 读取文档方法
function readText(file) {
const fs = require('fs');
return fs.readFileSync(file, 'utf8');
}
// 字数统计
function wordsCount() {
var str = txtEditor.value;
sLen = 0;
try{
//先将回车换行符做特殊处理
str = str.replace(/(\r\n+|\s+| +)/g,"龘");
//处理英文字符数字,连续字母、数字、英文符号视为一个单词
str = str.replace(/[\x00-\xff]/g,"m");
//合并字符m,连续字母、数字、英文符号视为一个单词
str = str.replace(/m+/g,"*");
//去掉回车换行符
str = str.replace(/龘+/g,"");
//返回字数
sLen = str.length;
}catch(e){
console.log(e);
}
// 刷新当前字数统计值到页面中
document.getElementById("txtNum").innerHTML = sLen;
}
// 拖拽读取文档
const dragContent = document.querySelector('#txtEditor');
// 阻止 electron 默认事件
dragContent.ondragenter = dragContent.ondragover = dragContent.ondragleave = function() {
return false;
}
// 拖拽事件执行
dragContent.ondrop = function(e) {
e.preventDefault(); // 阻止默认事件
currentFile = e.dataTransfer.files[0].path; // 获取文档路径
const txtRead = readText(currentFile);
txtEditor.value = txtRead;
document.title = 'Notepad - ' + currentFile;
isSave = true;
wordsCount();
}
主页面代码 index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Notepad</title>
<style type="text/css">
body,html{
margin:0;
padding:0;
}
#txtEditor{
width:786px;
height:539px;
padding:4px;
margin:0px;
border:0px;
font-size: 16px;
resize:none;
outline:none;
}
#txtEditor:focus{
border:0px;
outline:none;
}
.bottom {
height: 20px;
font-size: 12px;
color: #666666;
text-align: right;
padding-right: 20px;
display: block;
}
</style>
</head>
<body>
<textarea id="txtEditor"></textarea>
<div class="bottom">字数:<span id="txtNum">0</span></div>
<script src="./renderer.js"></script>
</body>
</html>
开发思路
代码量对于新手来说已经非常多了,但实际上使用的都是非常基础而且容易阅读的格式,而且我都一一做了注释,建议像我这样的新手,可以采用逐个功能击破的方式,一点点了解代码原理,比如逐个完成主菜单中的项目,每个功能模块都逐一调通,有问题可以留言,我都会尽可能回复,虽然我还是个初学者^_^。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。