Vite ❤ Electron——基於Vite搭建Electron+Vue3的開發環境【一】

liulun發表於2020-12-02

背景

目前社群兩大Vue+Electron的腳手架:electron-vuevue-cli-plugin-electron-builder

都有這樣那樣的問題,且都還不支援Vue3,然而Vue3已是大勢所趨,

Vite勢必也將成為官方Vue腳手架,

下圖是尤雨溪在開發好Vite之後與webpack之父的對話

 

 所以開發一個Vite+Vue3+Electron的腳手架的需求日趨強烈

我前段時間做了一個,

但是發現了一些與Vite有關的問題,

比如:Vite會把開發環境的process物件吃掉的問題

這對於web專案來說問題不大,但對於我們的Electron專案來說,就影響很大了

今天我就把這個思路和實現方式的關鍵程式碼發出來供大家參考,

同時也希望Vue社群的貢獻者們,能注意到這個問題

(給Vue官方的各個專案提issue真的是太難了,Electron官方專案在這方面就做的很好,很open、很包容)

環境

先用Vite建立一個Vue3的工程,這就是你的實際專案工程

接著安裝幾個Electron相關的依賴,最終我的工程下的依賴情況如下:

    "@vue/compiler-sfc": "^3.0.0",
    "vite": "^1.0.0-rc.9",
    "vue": "^3.0.2",
    "vue-router": "^4.0.0-rc.1",
    "electron": "^11.0.2",
    "electron-builder": "^22.9.1",
    "electron-updater": "^4.3.5",
    "postcss-scss": "^3.0.2",
 
    "sass": "^1.27.0",

注意:這些依賴全部安裝在devDependencies下

各個庫的版本發文時應該是最新的了,不過如果有更新的版本,你完全可以用,沒影響。

工程的目錄結構大概是如下這樣:

[yourProject]

  node_modules  依賴包

  public  vite建立的目錄,為vue服務的,實際沒多大用

  release  打包後編譯輸出的目錄,該目錄的根目錄下存放打包後的安裝包

    bundled  該目錄存放vue打包後的檔案(html js css img等)

    win-unpacked  該目錄存放編譯後生成的可執行檔案及相關的dll,不包含安裝包

  resource  資源目錄

    unrelease  該目錄存放編譯期需要的資源

    release   該目錄存放編譯後需要隨安裝包分發給客戶的資源

  script     此目錄存放各種指令碼,比如編譯指令碼,啟動指令碼,簽名指令碼等

  src  原始碼目錄

    render  渲染程式原始碼目錄

    main  主程式原始碼目錄

    common  兩個程式都會用到的共用原始碼目錄

  package.json  專案配置檔案

  index.html  vue3的入口頁面

  .gitignore

接著在package.json中,增加兩個命令:

  "scripts": {
    "start": "node ./script/dev.js",
    "release": "node ./script/release.js"
  },

同時在script目錄下建立相應的檔案,接著我們就開始撰寫者兩個檔案的程式碼了

除錯指令碼

通過Vite啟動Web專案

除錯指令碼首先要做的工作就是啟動Vue專案

讓它跑在http://localhost下,這樣我們修改渲染程式的程式碼時,

會通過Vite的熱更新機制實時反饋到介面上

Vite除了提供cli的指令啟動專案外,也提供了API,我這裡就是直接調它的API來啟動專案的

關鍵程式碼如下:

let vite = require("vite")
  createServer () {
    return new Promise((resolve, reject) => {
      let options = {
        root:process.cwd(),
        enableEsbuild: true
     };
      this.server = vite.createServer(options);
      this.server.on("error", (e) => this.serverOnErr(e));
      this.server.on("data", (e) => console.log(e.toString()));
      this.server.listen(this.serverPort, () => {
        console.log(`http://localhost:${this.serverPort}`);
        resolve();
      });
    });
  }, 

其中this.serverPort是繫結在當前物件上的一個變數,意義是指定vite專案啟動時使用的埠號

啟動成功後http server物件繫結到當前物件的server變數上

如果啟動過程中報錯,則很有可能是埠占用,將執行如下邏輯:

  serverOnErr (err) {
    if (err.code === "EADDRINUSE") {
      console.log(
        `Port ${this.viteServerPort} is in use, trying another one...`
      );
      setTimeout(() => {
        this.server.close();
        this.serverPort += 1;
        this.server.listen(this.viteServerPort);
      }, 100);
    } else {
      console.error(chalk.red(`[vite] server error:`));
      console.error(err);
    }
  },

這段邏輯就是遞增埠號,再次嘗試啟動http server

設定環境變數

往往每個開發人員的環境變數都是不一樣的

有的開發人員需要連開發伺服器A,有的開發人員需要連開發伺服器B

而且開發環境的環境變數、測試環境、生產環境的環境變數也不一樣

所以我把環境變數設定到幾個單獨的檔案中

方便區分不同的環境,也方便gitignore,避免不同開發人員的環境變數互相沖突

開發環境的環境變數儲存在src/script/dev.env.js中

let env = require("./dev.env.js")

生產環境的環境變數則為release.env.js

這個檔案的程式碼非常簡單,如下:

module.exports = {
  APP_VERSION: require("../package.json").version,
  ENV_NOW: "dev",
  PROTOBUF_SERVER: "******.com",
  SENTRY_SERVICE: "https://******.com/34",
  ELECTRON_DISABLE_SECURITY_WARNINGS: true
}

需要注意的是:ELECTRON_DISABLE_SECURITY_WARNINGS,

這個環境變數是為了遮蔽Electron開發者除錯工具那一大堆警告的

(你如果開發過Electron應用,你應該知道我說的是什麼)

 APP_VERSION是從專案的package.json中取的版本號,

你當然可以不設定這個環境變數,通過Electron的API獲取版本號

app.getVersion() //主程式可用

但通過ElectronAPI獲取到的版本號,在開發環境下,是Electron.exe的版本號,不是你的專案的版本號

打包編譯後,這個問題是不存在的。

ENV_NOW是當前的環境,開發環境下它的值為dev,打包編譯後的生產環境它的值應為product,

因為現在我們是講如何構建開發環境,引用的是dev.env.js,

等下一篇文章講如何構建編譯環境時,引用的就是release.env.js了,

編譯主程式程式碼

Vite之所以快,有一個很重要的原因是它使用了esbuild模組來編譯程式碼

這裡我們也使用esbuild來編譯我們的主程式的程式碼

前面說了主程式是放在src/main/目錄下的

這裡我使用的是TypeScript開發,入口程式是app.ts,你完全可以使用Js開發,檔名也隨你自定義

  buildMain () {
    let outfile = path.join(this.bundledDir, "entry.js");
    let entryFilePath = path.join(process.cwd(), "src/main/app.ts");
    //這個方法得到的結果:{outputFiles: [ { contents: [Uint8Array], path: '<stdout>' } ]}
    esbuild.buildSync({
      entryPoints: [entryFilePath],
      outfile,
      minify: false,
      bundle: true,
      platform: "node",
      sourcemap: false,
      external: ["electron"],
    });
    env.WEB_PORT = this.serverPort;
    let envScript = `process.env={...process.env,...${JSON.stringify(env)}};`
    let js = `${envScript}${os.EOL}${fs.readFileSync(outfile)}`;
    fs.writeFileSync(outfile, js)
  },

esbuild會自動查詢app.ts引用的其他程式碼,

還有treeshaking機制保證你不會把無用的程式碼打包到輸出目錄

我把sourcemap關掉了,因為除錯主程式很困難,

基本都是手動console.log資訊除錯的,朋友們有好的建議請賜教一下

platform要指定成node,要不然esbuild會嘗試幫你去找node.js內建的包,肯定找不到,就報錯了

同理,還要把electron設定成external

在上一節設定的環境變數的基礎上

我們又增加了一個WEB_PORT的環境變數,

Electron啟動後,要根據這個變數去載入localhost的頁面,

這個變數是應用啟動時確定的,是動態的,所以沒辦法設定到dev.env.js中

輸出程式碼前,我們把環境變數的值也附加在輸出程式碼中了

這樣Electron程式啟動時,會先設定好環境變數,再執行具體的業務程式碼

(我們當然也可以通過其他方式設定環境變數,但這樣做主要是為了和生產環境保持一致,看到下一篇文章你就會知道了)

最終生成的程式碼會被輸出到這個目錄下面:

bundledDir: path.join(process.cwd(), "release/bundled")

稍後我們啟動Electron時,也會讓Electron載入這個目錄下的入口程式。

啟動Electron

Electron的node module並沒有提供API給開發者呼叫以啟動程式

所以我們只能通過node的child_process模組來啟動Electron的程式

程式碼如下:

  createElectronProcess () {
    this.electronProcess = spawn(
      require("electron").toString(),
      [path.join(this.bundledDir, "entry.js")],
      {
        cwd: process.cwd(),
        env,
      }
    );
    this.electronProcess.on("close", () => {
      this.server.close();
      process.exit();
    });
    this.electronProcess.stdout.on("data", (data) => {
      data = data.toString();
      console.log(data);
    });
  },

require("electron").toString()得到的是Electron的可執行檔案的路徑

Windows環境下為:node_modules\electron\dist\electron.exe

Mac環境下為:node_modules/electron/dist/Electron.app/Contents/MacOS/Electron

path.join(this.bundledDir, "entry.js")為Electron程式指定了入口程式檔案的地址

cwd: process.cwd()是為Electron指定當前工作目錄(此處又為Electron指定了一次環境變數,其實不指定也沒關係)

當Electron程式退出時,我們也關閉了Vite建立的http server

主程式載入渲染程式頁面

此處最關鍵的邏輯就是這一句

    if (process.env.ENV_NOW === "dev") {
      await win.loadURL(`http://localhost:${process.env.WEB_PORT}/`);
    }

process.env.WEB_PORT就是我們上文中設定的WEB_PORT變數

這個邏輯當然還有else分支,那是下一篇博文的內容了

敬請期待!

 

相關文章