如何開發Vite3外掛構建Electron開發環境

2022-11-11 09:00:12

新使用者購買《Electron + Vue 3 桌面應用開發》,加小冊專屬微信群,參與群抽獎,送《深入淺出Electron》、《Electron實戰》作者簽名版。
1等獎:《深入淺出Electron》+《Electron實戰》
2等獎:《深入淺出Electron》
3等獎:《Electron實戰》
抽獎活動是掘金組織的,僅限近幾日加入微信群的新成員(目前人還不多),我負責抽獎、郵寄,11月20日開始抽獎。凡參與抽獎的讀者都有機會中獎。

開發新版本 Vue 專案推薦你使用 Vite 腳手架構建開發環境,然而 Vite 腳手架更傾向於構建純 Web 頁面,而不是桌面應用,因此開發者要做很多額外的設定和開發工作才能把 Electron 引入到 Vue 專案中,這也是很多開發者都基於開源工具來構建 Electron+Vue 的開發環境的原因。

但這樣做有兩個問題:第一個是這些開源工具封裝了很多技術細節,導致開發者想要修改某項設定非常不方便;另一個是這些開源工具的實現方式我認為也並不是很好。

所以,我還是建議你儘量 自己寫程式碼構建 Electron+Vue 的開發環境 ,這樣可以讓自己更從容地控制整個專案。

具體應該怎麼做呢?接下來我將帶你按如下幾個步驟構建一個 Vite+Electron 的開發環境:

建立專案

首先通過命令列建立一個 Vue 專案:

npm create vite@latest electron-jue-jin -- --template vue-ts

接著安裝 Electron 開發依賴:

npm install electron -D

安裝完成後,你的專案根目錄下的 package.json 檔案應該與下面大體類似:

{
  "name": "electron-jue-jin",
  "private": true,
  "version": "0.0.1",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview"
  },
  "dependencies": {},
  "devDependencies": {
    "vue": "^3.2.37",
    "@vitejs/plugin-vue": "^3.0.0",
    "electron": "^19.0.8",
    "typescript": "^4.6.4",
    "vite": "^3.0.0",
    "vue-tsc": "^0.38.4"
  }
}

注意:這裡我們 把 vue 從 dependencies 設定節移至了 devDependencies 設定節。這是因為在 Vite 編譯專案的時候,Vue 庫會被編譯到輸出目錄下,輸出目錄下的內容是完整的,沒必要把 Vue 標記為生產依賴;而且在我們將來製作安裝包的時候,還要用到這個 package.json 檔案,它的生產依賴裡不應該有沒用的東西,所以我們在這裡做了一些調整。

到這裡,我們就建立了一個基本的 Vue+TypeScript 的專案,接下來我們就為這個專案引入 Electron 模組。

建立主程序程式碼

建立好專案之後,我們建立主程序的入口程式:src\main\mainEntry.ts。

這個入口程式的程式碼很簡單,如下所示:

//src\main\mainEntry.ts
import { app, BrowserWindow } from "electron";

let mainWindow: BrowserWindow;

app.whenReady().then(() => {
  mainWindow = new BrowserWindow({});
  mainWindow.loadURL(process.argv[2]);
});

在這段程式碼裡,我們在 app ready 之後建立了一個簡單的 BrowserWindow 物件。app 是 Electron 的全域性物件,用於控制整個應用程式的生命週期。在 Electron 初始化完成後,app 物件的 ready 事件被觸發,這裡我們使用 app.whenReady() 這個 Promise 方法來等待 ready 事件的發生。

mainWindow 被設定成一個全域性變數,這樣可以避免主視窗被 JavaScript 的垃圾回收器回收掉。另外,視窗的所有設定都使用了預設的設定。

這個視窗載入了一個 Url 路徑,這個路徑是以命令列引數的方式傳遞給應用程式的,而且是命令列的第三個引數。

app 和 BrowserWindow 都是 Electron 的內建模組,這些內建模組是通過 ES Module 的形式匯入進來的,我們知道 Electron 的內建模組都是通過 CJS Module 的形式匯出的,這裡之所以可以用 ES Module 匯入,是因為我們接下來做的主程序編譯工作幫我們完成了相關的轉化工作。

開發環境 Vite 外掛

主程序的程式碼寫好之後,只有編譯過之後才能被 Electron 載入,我們是 通過 Vite 外掛的形式來完成這個編譯工作和載入工作 的,如下程式碼所示:

//plugins\devPlugin.ts
import { ViteDevServer } from "vite";
export let devPlugin = () => {
  return {
    name: "dev-plugin",
    configureServer(server: ViteDevServer) {
      require("esbuild").buildSync({
        entryPoints: ["./src/main/mainEntry.ts"],
        bundle: true,
        platform: "node",
        outfile: "./dist/mainEntry.js",
        external: ["electron"],
      });
      server.httpServer.once("listening", () => {
        let { spawn } = require("child_process");
        let addressInfo = server.httpServer.address();
        let httpAddress = `http://${addressInfo.address}:${addressInfo.port}`;
        let electronProcess = spawn(require("electron").toString(), ["./dist/mainEntry.js", httpAddress], {
          cwd: process.cwd(),
          stdio: "inherit",
        });
        electronProcess.on("close", () => {
          server.close();
          process.exit();
        });
      });
    },
  };
};

這是一個簡單的 Vite 外掛,在這個外掛中我們註冊了一個名為 configureServer 的勾點,當 Vite 為我們啟動 Http 服務的時候,configureServer勾點會被執行

這個勾點的輸入引數為一個型別為 ViteDevServer 的物件 server,這個物件持有一個 http.Server 型別的屬性 httpServer,這個屬性就代表著我們偵錯 Vue 頁面的 http 服務,一般情況下地址為:http://127.0.0.1:5173/

我們可以 通過監聽 server.httpServerlistening 事件來判斷 httpServer 是否已經成功啟動,如果已經成功啟動了,那麼就啟動 Electron 應用,並給它傳遞兩個命令列引數,第一個引數是主程序程式碼編譯後的檔案路徑,第二個引數是 Vue 頁面的 http 地址,這裡就是 http://127.0.0.1:5173/

為什麼這裡傳遞了兩個命令列引數,而主程序的程式碼接收第三個引數(process.argv[2])當做 http 頁面的地址呢?因為 預設情況下 electron.exe 的檔案路徑將作為第一個引數。也就是我們通過 require("electron") 獲得的字串。

這個路徑一般是:node_modules\electron\dist\electron.exe,如果這個路徑下沒有對應的檔案,說明你的 Electron 模組沒有安裝好。

我們是 通過 Node.js child_process 模組的 spawn 方法啟動 electron 子程序的,除了兩個命令列引數外,還傳遞了一個設定物件。

這個物件的 cwd 屬性用於設定當前的工作目錄,process.cwd() 返回的值就是當前專案的根目錄。stdio 用於設定 electron 程序的控制檯輸出,這裡設定 inherit 可以讓 electron 子程序的控制檯輸出資料同步到主程序的控制檯。這樣我們在主程序中 console.log 的內容就可以在 VSCode 的控制檯上看到了。

當 electron 子程序退出的時候,我們要關閉 Vite 的 http 服務,並且控制父程序退出,準備下一次啟動。

http 服務啟動之前,我們 使用 esbuild 模組完成了主程序 TypeScript 程式碼的編譯工作 ,這個模組是 Vite 自帶的,所以我們不需要額外安裝,可以直接使用。

主程序的入口檔案是通過 entryPoints 設定屬性設定的,編譯完成後的輸出檔案時通過 outfile 屬性設定的。

編譯平臺 platform 設定為 node,排除的模組 external 設定為 electron正是這兩個設定使我們可以在主程序程式碼中可以通過 import 的方式匯入 electron 內建的模組 。非但如此,Node 的內建模組也可以通過 import 的方式引入。

這個 Vite 外掛的程式碼編寫好後,在 vite.config.ts 檔案中引入一下就可以使用了,如下程式碼所示:

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { devPlugin } from "./plugins/devPlugin";
import optimizer from "vite-plugin-optimizer";

export default defineConfig({
  plugins: [devPlugin(), vue()],
});

現在執行命令 npm run dev,你會看到 Electron 應用載入了 Vue 的首頁,如下圖所示:

關閉視窗,主程序和子程序也會跟著退出。修改一下 Vue 元件裡的內容,視窗內顯示的內容也會跟著變化,說明熱更新機制在起作用。

渲染程序整合內建模組

現在主程序內可以自由的使用 Electron 和 Node.js 的內建模組了,但渲染程序還不行,接下去我們就為渲染程序整合這些內建模組。

首先我們修改一下主程序的程式碼,開啟渲染程序的一些開關,允許渲染程序使用 Node.js 的內建模組,如下程式碼所示:

// src\main\mainEntry.ts
import { app, BrowserWindow } from "electron";
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
let mainWindow: BrowserWindow;

app.whenReady().then(() => {
  let config = {
    webPreferences: {
      nodeIntegration: true,
      webSecurity: false,
      allowRunningInsecureContent: true,
      contextIsolation: false,
      webviewTag: true,
      spellcheck: false,
      disableHtmlFullscreenWindowResize: true,
    },
  };
  mainWindow = new BrowserWindow(config);
  mainWindow.webContents.openDevTools({ mode: "undocked" });
  mainWindow.loadURL(process.argv[2]);
});

在這段程式碼中,有以下幾點需要注意:

1:ELECTRON_DISABLE_SECURITY_WARNINGS 用於設定渲染程序開發者偵錯工具的警告,這裡設定為 true 就不會再顯示任何警告了。

如果渲染程序的程式碼可以存取 Node.js 的內建模組,而且渲染程序載入的頁面(或指令碼)是第三方開發的,那麼惡意第三方就有可能使用 Node.js 的內建模組傷害終端使用者 。這就是為什麼這裡要有這些警告的原因。如果你的應用不會載入任何第三方的頁面或指令碼。那麼就不用擔心這些安全問題啦。

2:nodeIntegration設定項的作用是把 Node.js 環境整合到渲染程序中,contextIsolation設定項的作用是在同一個 JavaScript 上下文中使用 Electron API。其他設定項與本文主旨無關,大家感興趣的話可以自己翻閱官方檔案。

3: webContentsopenDevTools方法用於開啟開發者偵錯工具

完成這些工作後我們就可以在開發者偵錯工具中存取 Node.js 和 Electron 的內建模組了。

設定 Vite 模組別名與模組解析勾點

雖然我們可以在開發者偵錯工具中使用 Node.js 和 Electron 的內建模組,但現在還不能在 Vue 的頁面內使用這些模組。

這是因為 Vite 主動遮蔽了這些內建的模組,如果開發者強行引入它們,那麼大概率會得到如下報錯:

Module "xxxx" has been externalized for browser compatibility and cannot be accessed in client code.

接下去我們就介紹如何讓 Vite 載入 Electron 的內建模組和 Node.js 的內建模組。

首先我們為工程安裝一個 Vite 元件:vite-plugin-optimizer

npm i vite-plugin-optimizer -D

然後修改 vite.config.ts 的程式碼,讓 Vite 載入這個外掛,如下程式碼所示:

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { devPlugin, getReplacer } from "./plugins/devPlugin";
import optimizer from "vite-plugin-optimizer";

export default defineConfig({
  plugins: [optimizer(getReplacer()), devPlugin(), vue()],
});

vite-plugin-optimizer 外掛會為你建立一個臨時目錄:node_modules.vite-plugin-optimizer

然後把類似 const fs = require('fs'); export { fs as default } 這樣的程式碼寫入這個目錄下的 fs.js 檔案中。

渲染程序執行到:import fs from "fs" 時,就會請求這個目錄下的 fs.js 檔案,這樣就達到了在渲染程序中引入 Node 內建模組的目的。

getReplacer 方法是我們為 vite-plugin-optimizer 外掛提供的內建模組列表。程式碼如下所示:

// plugins\devPlugin.ts
export let getReplacer = () => {
  let externalModels = ["os", "fs", "path", "events", "child_process", "crypto", "http", "buffer", "url", "better-sqlite3", "knex"];
  let result = {};
  for (let item of externalModels) {
    result[item] = () => ({
      find: new RegExp(`^${item}$`),
      code: `const ${item} = require('${item}');export { ${item} as default }`,
    });
  }
  result["electron"] = () => {
    let electronModules = ["clipboard", "ipcRenderer", "nativeImage", "shell", "webFrame"].join(",");
    return {
      find: new RegExp(`^electron$`),
      code: `const {${electronModules}} = require('electron');export {${electronModules}}`,
    };
  };
  return result;
};

我們在這個方法中把一些常用的 Node 模組和 electron 的內建模組提供給了 vite-plugin-optimizer 外掛,以後想要增加新的內建模組只要修改這個方法即可。而且 vite-plugin-optimizer 外掛不僅用於開發環境,編譯 Vue 專案時,它也會參與工作

再次執行你的應用,看看現在渲染程序是否可以正確載入內建模組了呢?你可以通過如下程式碼在 Vue 元件中做這項測試:

//src\App.vue
import fs from "fs";
import { ipcRenderer } from "electron";
import { onMounted } from "vue";
onMounted(() => {
  console.log(fs.writeFileSync);
  console.log(ipcRenderer);
});

不出意外的話,開發者偵錯工具將會輸出如下內容:

總結

現在我們邁出了萬里長征的第一步,構建好了 Vue3+Vite3+Electron 的開發環境 ,而且完成這項工作並不依賴於市面上任何一個現成的構建工具,這個開發環境是我們自己動手一點一點搭起來的,以後我們想增加或者修改一項功能,都可以很從容地自己動手處理。

非但如此,我們還通過本講內容向你介紹了 Vite 外掛的開發技巧和如何建立一個簡單的 Electron 應用等知識。下一講我們將在本節課的基礎上,進一步介紹如何使用 Vite 外掛製作 Electron 應用的安裝包。