一名UI設計師的Electron學習之路(二)——記事本APP

最近一直在摸索Electron,網上有大牛使用Electron-vue作的一些應用,但因爲本人非科班開發人員,學習起來老是雲裏霧裏的,最終仍是迴歸原始的Electron開發,再逐步拓展其餘棧的知識。好了,接下來是最近臨摹的一個Electron記事本,原文是簡書的做者鰻駝螺寫的一個教程————從零開始寫一個記事本app,地址 https://www.jianshu.com/p/57d910008612/css

因爲做者寫的時候是2017年,版本已經很是久遠了,第一次寫的時候愣是沒跑起來,並且功能相對簡單,想要做爲平時使用仍是有點欠缺的,所以本人對該記事本App作了一些優化,增長了文件拖放讀取,字數統計和另存爲功能,用起來也仍是能夠的,目前禁用了最大化和自由縮放窗體的功能,由於一些適配效果不理想,所以暫時放棄該功能,先來看下效果吧。html

記事本效果圖.gif


知識點整理

用到的知識點比較多,主要是vue

  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
<!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>
開發思路

代碼量對於新手來講已經很是多了,但實際上使用的都是很是基礎並且容易閱讀的格式,並且我都一一作了註釋,建議像我這樣的新手,能夠採用逐個功能擊破的方式,一點點了解代碼原理,好比逐個完成主菜單中的項目,每一個功能模塊都逐一調通,有問題能夠留言,我都會盡量回復,雖然我仍是個初學者^_^。html5

完整項目地址

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

相關文章
相關標籤/搜索