基于 Vite 搭建 Electron + Vue3 的开发环境
目前社区两大 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、很包容)。
"@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 下
各个库的版本发文时应该是最新的了,不过如果有更新的版本,你完全可以用,没影响。工程的目录结构大概是如下这样:
"scripts": {
"start": "node ./script/dev.js",
"release": "node ./script/release.js"
},
同时在 script 目录下创建相应的文件,接着我们就开始撰写者两个文件的代码了
调试脚本首先要做的工作就是启动 Vue 项目,让它跑在 http://localhost 下,这样我们修改渲染进程的代码时,会通过 Vite 的热更新机制实时反馈到界面上。
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。
往往每个开发人员的环境变量都是不一样的,有的开发人员需要连开发服务器 A,有的开发人员需要连开发服务器 B,而且开发环境的环境变量、测试环境、生产环境的环境变量也不一样,所以我把环境变量设置到几个单独的文件中,方便区分不同的环境,也方便 gitignore,避免不同开发人员的环境变量互相冲突。
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。这个环境变量是为了屏蔽 Electron 开发者调试工具那一大堆警告的(你如果开发过 Electron 应用,你应该知道我说的是什么),APP_VERSION 是从项目的 package.json 中取的版本号,你当然可以不设置这个环境变量,通过 Electron 的 API 获取版本号:
app.getVersion() // 主进程可用
但通过 ElectronAPI 获取到的版本号,在开发环境下,是 Electron.exe 的版本号,不是你的项目的版本号,打包编译后,这个问题是不存在的。
ENV_NOW 是当前的环境,开发环境下它的值为 dev,打包编译后的生产环境它的值应为 product,因为现在我们是讲如何构建开发环境,引用的是 dev.env.js,等下一篇文章讲如何构建编译环境时,引用的就是 release.env.js 了。
buildMain () {
let outfile = path.join(this.bundledDir, "entry.js");
let entryFilePath = path.join(process.cwd(), "src/main/app.ts");
// 这个方法得到的结果:{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 机制保证你不会把无用的代码打包到输出目录。我把 sourcemap 关掉了,因为调试主进程很困难,基本都是手动 console.log 信息调试的,朋友们有好的建议请赐教一下。platform 要指定成 node,要不然 esbuild 会尝试帮你去找 node.js 内置的包,肯定找不到,就报错了。
同理,还要把 electron 设置成 external,在上一节设置的环境变量的基础上,我们又增加了一个 WEB_PORT 的环境变量,Electron 启动后,要根据这个变量去加载 localhost 的页面,这个变量是应用启动时确定的,是动态的,所以没办法设置到 dev.env.js 中,输出代码前,我们把环境变量的值也附加在输出代码中了。
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 的可执行文件的路径:
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 分支,那是下一篇博文的内容了。
敬请期待!