1. 程式人生 > >使用 electron-vue 搭建桌面應用開發模板

使用 electron-vue 搭建桌面應用開發模板

參考 PicGo 搭建符合本公司需求的桌面應用開發模板

已實現功能:

1.單行命令即可生成可安裝程式

2.使用 nsis 構建安裝嚮導

3.實現檔案的讀寫功能

4.視窗的最小化按鈕和關閉按鈕以及標題欄自定義,不使用 electron 自身攜帶的原生標題欄

5.視窗關閉儲存到托盤

6.托盤右鍵選單有'關於本產品'...選單

7.使用說明可在獨立視窗中開啟,且是以本地 pdf 形式

8.使用 nsis 製作安裝嚮導,實現證書自動安裝

實現過程:

1.單行命令即可生成可安裝程式

    構建專案的時候選擇 electron-builder ,執行 npm run build 之後自動打包成 setup.exe 可安裝檔案

2.使用 nsis 構建安裝嚮導

    參考蘇南大叔的‘如何利用 nsis 製作 electron 的安裝包’即可,electron-builder 外掛也有 nsis 配置項,但是侷限性有點大且不可控

3.實現檔案的讀寫功能

// 在 js 中定義 files 讀寫檔案
import path from "path";
import fs from "fs-extra";
import { app, remote } from "electron"; // 引入remote模組

const APP = process.type === "renderer" ? remote.app : app; // 根據process.type來分辨在哪種模式使用哪種模組

const STORE_PATH = APP.getPath("userData"); // 獲取electron應用的使用者目錄
if (!fs.pathExistsSync(STORE_PATH)) {
  // 如果不存在路徑
  fs.mkdirpSync(STORE_PATH); // 就建立
}

export const files = {
  read: function(filesName) {
    const path_ = path.join(STORE_PATH, filesName);
    let filesData = fs.readFileSync(path_, "utf-8", function(e, data) {
      if (e) throw e;
      return data;
    });
    return filesData;
  },
  write: function(filesName, writeStr) {
    const path_ = path.join(STORE_PATH, filesName);
    fs.open(path_, "w", function(e, fd) {
      if (e) throw e;
      fs.write(fd, writeStr, 0, "utf8", function(e) {
        if (e) throw e;
        fs.closeSync(fd);
      });
    });
  }
};

呼叫:

import { files } from "@/scripts/fileOpera.js";
const {
  ipcRenderer
} = window.require("electron")
export default {
  name: "landing-page",
  data() {
    return {
      filesData:''
    };
  },
  methods: {
    writeFile: function() {  // 寫入 userSet.txt 檔案
      files.write(
        "/userSet.txt",
        'write some to test'
      );
    },
    readFile: function() {  // 讀取 userSet.txt 檔案
      this.filesData = files.read("/userSet.txt");
    },
    openUseDirections: function(){  // render 程序與 main 程序互動開啟使用說明視窗
      ipcRenderer.send("openUseDirections");
    }
  }
};
4.視窗的最小化按鈕和關閉按鈕以及標題欄自定義,不使用 electron 自身攜帶的原生標題欄

    首先在建立視窗的時候需要把 frame 配置項設定為 false,其次在 App.vue 元件中自定義標題欄以及右側按鈕

HTML

<template>
  <div id="app">
    <div class="fake-title-bar">
      <div class="title-mark">
        <img src="../../static/menubar-nodarwin.png" />
        標題
      </div>
      <div class="handle-bar" v-if="os === 'win32'">
        <Icon type="minus" @click="minimizeWindow"></Icon>
        <Icon type="close" @click="closeWindow"></Icon>
      </div>
    </div>
    <router-view></router-view>
  </div>
</template>

JS

<script>
import { remote } from "electron";
import { myMixin } from './mixins/test'
const { BrowserWindow } = remote;
import {time_differ, toISOString, formatDate} from '@/scripts/times.js'
export default {
  name: "buildertest",
  mixins: [myMixin],
  data() {
    return {
      os: ""
    };
  },
  created() {
    this.os = process.platform;
  },
  methods: {
    minimizeWindow() {
      const window = BrowserWindow.getFocusedWindow();
      window.minimize();
    },
    closeWindow() {
      const window = BrowserWindow.getFocusedWindow();      
      window.close();
    }
  }
};
</script>

STYLES

<style lang='less'>
#app {
  .fake-title-bar {
    -webkit-app-region: drag;
    height: h = 24px;
    color: #2c2c2c;
    font-size: 12px;
    line-height: h;
    width: 100%;
    border-bottom: 1px solid #d8d8d8;
    position: fixed;
    z-index: 100;

    .title-mark {
      position: absolute;
      left: 4px;
      top: 0;
      z-index: 10000;
      padding-left: 26px;

      img {
        position: absolute;
        top: 50%;
        left: 4px;
        transform: translateY(-50%);
        height: 20px;
      }
    }

    .handle-bar {
      position: absolute;
      top: 2px;
      right: 4px;
      width: 40px;
      height: h;
      z-index: 10000;
      -webkit-app-region: no-drag;

      i {
        cursor: pointer;
        font-size: 16px;
      }

      .ivu-icon-minus {
        margin-right: 6px;

        &:hover {
          color: #409EFF;
        }
      }

      .ivu-icon-close {
        &:hover {
          color: #F15140;
        }
      }
    }
  }

  .writeFile {
    position: absolute;
    top: 40px;
    font-size: 14px;
  }
}
</style>

5.視窗關閉儲存到托盤

6.托盤右鍵選單有'關於本產品'...選單

// 定義 isQuit 變數儲存當前關閉是視窗觸發還是右鍵托盤退出選單觸發
let isQuit = false; // 預設是從視窗觸發

// 建立托盤選單
function createTray() {
  const menubarPic =
    process.platform === "darwin"
      ? `${__static}/menubar.png`
      : `${__static}/menubar-nodarwin.png`;
  tray = new Tray(menubarPic);
  contextMenu = Menu.buildFromTemplate([
    {
      label: "關於",
      click() {
        dialog.showMessageBox({
          title: "xxx",
          message: "xxx",
          detail: `版本: ${pkg.version}\n`
        });
      }
    },
    {
      label: "開啟",
      click() {
        if (mainWindow === null) {
          createWindow();
          mainWindow.show();
          mainWindow.maximize();
        } else {
          mainWindow.show();
          mainWindow.maximize();
        }
      }
    },
    {
      label: "退出",
      click: function() {
        isQuit = true;
        app.quit();
        app.quit(); // 程式設定關閉為最小化,所以呼叫兩次關閉,防止最大化時一次不能關閉
      }
    }
  ]);
  // 設定此托盤圖表的懸停提示內容
  tray.setToolTip("xxx產品名稱");

  tray.on("right-click", () => {
    tray.popUpContextMenu(contextMenu);
  });
  tray.on("click", () => {
    if (mainWindow === null) {
      createWindow();
      mainWindow.show();
      mainWindow.maximize();
    } else {
      mainWindow.show();
      mainWindow.maximize();
    }
  });
}

// 監聽關閉事件
mainWindow.on("close", function(event) {
    if (!isQuit) {
      event.preventDefault();
      mainWindow.hide();
    }
    return false;
  });

// 點選圖示(桌面快捷方式)檢查當前活動例項的個數
const isSecondInstance = app.makeSingleInstance(() => {
  if (mainWindow) {
    if (mainWindow.isMinimized()) {
      mainWindow.restore(); // 視窗從最小化恢復時觸發
    }
    mainWindow.show();
    mainWindow.maximize();
    mainWindow.focus();
  }
});

if (isSecondInstance) {
  app.quit();
}

7.使用說明可在獨立視窗中開啟,且是以本地 pdf 形式

// 定義 useDirection 儲存使用說明視窗例項
let useDirection = null;
// 使用 ipcMain 與 ipcRender 互動
ipcMain.on("openUseDirections", (event) => {
  let path_ = app.getAppPath().split("\\").join("/");
  if(path_.indexOf("app.asar") !== -1){
    // 將目錄最後的 /app.asar 去除
    path_ = path_.substr(0,path_.lastIndexOf('/'))
  }
  Menu.setApplicationMenu(null);//隱藏選單
  if (useDirection) {
    if (useDirection.isMinimized()) {
      useDirection.restore(); // 視窗從最小化恢復時觸發
    }
    useDirection.show();
    useDirection.focus();
  }else{
    let options = {
      width: 838,
      height: 600,
      icon:`${__static}/icon.ico`,
      title:'xxx',
      autoHideMenuBar:true,
      webPreferences: {
        plugins: true
      }
    };
    useDirection = new PDFWindow(options);
    useDirection.loadURL(`file:///${path_}/static/xxx.pdf`);
  }
  useDirection.on('closed', () => {
    useDirection = null;
  })
  event.sender.returnValue = false;
})

說明:

1) app.getAppPath ()返回當前應用所在的路徑,使用 electron-builder 打包的安裝程式安裝之後返回的路徑後面帶有 /app.asar 這一結尾路徑,需要將此路徑去除在重新組合 static/xxx.pdf 路徑,因 electron-builder 打包的檔案全部是 asar 檔案,可以自己寫指令碼在打包之後新建 static 資料夾,將 xxx.pdf 拷貝進該資料夾,也可手動操作。

2.)新建開啟本地 pdf 檔案的視窗使用了  electron-pdf-window 外掛(基本原理是對 pdf.js 外掛進行再次封裝)

  •  安裝 npm install electron-pdf-window --save-dev
  • 引入 const PDFWindow = require("electron-pdf-window");

8.使用 nsis 製作安裝嚮導,實現證書自動安裝

因本公司自己的產品在客戶端安裝之後需要安裝本公司自己簽發的 rootCA.crt 證書,所以在使用 nsis 製作安裝嚮導的時候將執行 run.bat 指令碼(安裝當前目錄的 rootCA.crt 證書到'受信任的根證書頒發機構')

  • 使用蘇南大叔的‘如何利用 nsis 製作 electron 的安裝包’製作安裝嚮導
  • 將 run.bat 指令碼和 rootCA.crt 證書都拷貝到 resources/static 目錄下即可
  • 在 Section "MainSection" SEC01 指令碼的最後 SectionEnd 指令碼的前面新增  ExecShell "open" "$INSTDIR/resources/static/run.bat"