基于 Vite 搭建 Electron + Vue3 的開發(fā)環(huán)境

目前社區(qū)兩大 Vue+Electron 的腳手架:electron-vue 和 vue-cli-plugin-electron-builder。
都有這樣那樣的問題,且都還不支持 Vue3,然而 Vue3 已是大勢所趨,Vite 勢必也將成為官方 Vue 腳手架,下圖是尤雨溪在開發(fā)好 Vite 之后與 webpack 之父的對話:

所以開發(fā)一個 Vite+Vue3+Electron 的腳手架的需求日趨強(qiáng)烈。
我前段時間做了一個,但是發(fā)現(xiàn)了一些與 Vite 有關(guān)的問題,比如:Vite 會把開發(fā)環(huán)境的 process 對象吃掉的問題。
這對于 web 項目來說問題不大,但對于我們的 Electron 項目來說,就影響很大了。
今天我就把這個思路和實現(xiàn)方式的關(guān)鍵代碼發(fā)出來供大家參考,同時也希望 Vue 社區(qū)的貢獻(xiàn)者們,能注意到這個問題(給 Vue 官方的各個項目提 issue 真的是太難了,Electron 官方項目在這方面就做的很好,很 open、很包容)。
"@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 下
各個庫的版本發(fā)文時應(yīng)該是最新的了,不過如果有更新的版本,你完全可以用,沒影響。工程的目錄結(jié)構(gòu)大概是如下這樣:

"scripts": {
????"start":?"node ./script/dev.js",
????"release":?"node ./script/release.js"
??},同時在 script 目錄下創(chuàng)建相應(yīng)的文件,接著我們就開始撰寫者兩個文件的代碼了
調(diào)試腳本首先要做的工作就是啟動 Vue 項目,讓它跑在 http://localhost 下,這樣我們修改渲染進(jìn)程的代碼時,會通過 Vite 的熱更新機(jī)制實時反饋到界面上。
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();
??????});
????});
??},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。
往往每個開發(fā)人員的環(huán)境變量都是不一樣的,有的開發(fā)人員需要連開發(fā)服務(wù)器 A,有的開發(fā)人員需要連開發(fā)服務(wù)器 B,而且開發(fā)環(huán)境的環(huán)境變量、測試環(huán)境、生產(chǎn)環(huán)境的環(huán)境變量也不一樣,所以我把環(huán)境變量設(shè)置到幾個單獨的文件中,方便區(qū)分不同的環(huán)境,也方便 gitignore,避免不同開發(fā)人員的環(huán)境變量互相沖突。
let?env =?require("./dev.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。這個環(huán)境變量是為了屏蔽 Electron 開發(fā)者調(diào)試工具那一大堆警告的(你如果開發(fā)過 Electron 應(yīng)用,你應(yīng)該知道我說的是什么),APP_VERSION 是從項目的 package.json 中取的版本號,你當(dāng)然可以不設(shè)置這個環(huán)境變量,通過 Electron 的 API 獲取版本號:
app.getVersion()?// 主進(jìn)程可用但通過 ElectronAPI 獲取到的版本號,在開發(fā)環(huán)境下,是 Electron.exe 的版本號,不是你的項目的版本號,打包編譯后,這個問題是不存在的。
ENV_NOW 是當(dāng)前的環(huán)境,開發(fā)環(huán)境下它的值為 dev,打包編譯后的生產(chǎn)環(huán)境它的值應(yīng)為 product,因為現(xiàn)在我們是講如何構(gòu)建開發(fā)環(huán)境,引用的是 dev.env.js,等下一篇文章講如何構(gòu)建編譯環(huán)境時,引用的就是 release.env.js 了。
buildMain () {
????let?outfile = path.join(this.bundledDir,?"entry.js");
????let?entryFilePath = path.join(process.cwd(),?"src/main/app.ts");
????// 這個方法得到的結(jié)果:{outputFiles: [ { contents: [Uint8Array], path: '' } ]}
????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 機(jī)制保證你不會把無用的代碼打包到輸出目錄。我把 sourcemap 關(guān)掉了,因為調(diào)試主進(jìn)程很困難,基本都是手動 console.log 信息調(diào)試的,朋友們有好的建議請賜教一下。platform 要指定成 node,要不然 esbuild 會嘗試幫你去找 node.js 內(nèi)置的包,肯定找不到,就報錯了。
同理,還要把 electron 設(shè)置成 external,在上一節(jié)設(shè)置的環(huán)境變量的基礎(chǔ)上,我們又增加了一個 WEB_PORT 的環(huán)境變量,Electron 啟動后,要根據(jù)這個變量去加載 localhost 的頁面,這個變量是應(yīng)用啟動時確定的,是動態(tài)的,所以沒辦法設(shè)置到 dev.env.js 中,輸出代碼前,我們把環(huán)境變量的值也附加在輸出代碼中了。
bundledDir: path.join(process.cwd(),?"release/bundled")稍后我們啟動 Electron 時,也會讓 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 的可執(zhí)行文件的路徑:
Windows 環(huán)境下為:
node_modules\electron\dist\electron.exe
Mac 環(huán)境下為:
node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
path.join(this.bundledDir, "entry.js") 為 Electron 進(jìn)程指定了入口程序文件的地址,cwd: process.cwd() 是為 Electron 指定當(dāng)前工作目錄(此處又為 Electron 指定了一次環(huán)境變量,其實不指定也沒關(guān)系),當(dāng) Electron 進(jìn)程退出時,我們也關(guān)閉了 Vite 創(chuàng)建的 http server。
if?(process.env.ENV_NOW ===?"dev") {
??????await?win.loadURL(`http://localhost:${process.env.WEB_PORT}/`);
????}process.env.WEB_PORT 就是我們上文中設(shè)置的 WEB_PORT 變量。
這個邏輯當(dāng)然還有 else 分支,那是下一篇博文的內(nèi)容了。
敬請期待!
