一名UI设计师的Electron学习之路(二)——记事本APP

最近一直在摸索Electron,网上有大牛使用Electron-vue做的一些应用,但由于本人非科班开发人员,学习起来总是云里雾里的,最终还是回归原始的Electron开发,再逐步拓展其他栈的知识。好了,接下来是最近临摹的一个Electron记事本,原文是简书的作者鳗驼螺写的一个教程————从零开始写一个记事本app,地址 https://www.jianshu.com/p/57d910008612/

由于作者写的时候是2017年,版本已经非常久远了,第一次写的时候愣是没跑起来,而且功能相对简单,想要作为平时使用还是有点欠缺的,因此本人对该记事本App做了一些优化,增加了文件拖放读取,字数统计和另存为功能,用起来也还是可以的,目前禁用了最大化和自由缩放窗体的功能,因为一些适配效果不理想,因此暂时放弃该功能,先来看下效果吧。

一名UI设计师的Electron学习之路(二)——记事本APP_第1张图片


知识点整理

用到的知识点比较多,主要是

  1. main和renderer两个进程的通信
  2. electron 对话框 dialog 及 菜单 Menu 两个模块的使用
  3. nodejs的fs模块用于文本读写
  4. 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


  
    
    Notepad
    
  
  
    
    
字数:0
开发思路

代码量对于新手来说已经非常多了,但实际上使用的都是非常基础而且容易阅读的格式,而且我都一一做了注释,建议像我这样的新手,可以采用逐个功能击破的方式,一点点了解代码原理,比如逐个完成主菜单中的项目,每个功能模块都逐一调通,有问题可以留言,我都会尽可能回复,虽然我还是个初学者^_^。

完整项目地址

https://github.com/mongsel/Simple-Notepad

你可能感兴趣的:(electron,javascript,node.js,html5,css)