背景
目前社群兩大Vue+Electron的腳手架:electron-vue和vue-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",
注意:這些依賴全部安裝在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分支,那是下一篇博文的內容了
敬請期待!