当前位置: 首页 > news >正文

基于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、很包容)。

    环境    

先用 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 下

各个库的版本发文时应该是最新的了,不过如果有更新的版本,你完全可以用,没影响。工程的目录结构大概是如下这样:

接着在 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 分支,那是下一篇博文的内容了。

敬请期待!

《Electron实战:入门、进阶与性能优化》以实战为导向,讲解了如何用Electron结合现代前端技术来开发桌面应用。不仅全面介绍了Electron入门需要掌握的功能和原理,而且还针对Electron开发中的重点和难点进行了重点讲解,旨在帮助读者实现快速进阶。

作者刘晓伦,资深技术专家,有十余年研发经验,是Electron及其相关技术在企业应用领域的早期实践者,GitHub/Gitee优秀开源项目作者。作者还为Electron项目提交过数个Issue和Pull Request,均被官方接纳。

更多精彩回顾

书讯 | 12月书讯 | 年末上新,好书不断

资讯 | DB-Engines 12月数据库排名:PostgreSQL有望获得「2020年度数据库」荣誉?

书单 | 机器人时代已来!推荐几本机器人学硬核好书

干货 | Python循环语句代码逐行详解:while、for、break和continue

收藏 | 微信第 1 行代码曝光!

上新 | 复杂的密码学也可以人人可懂

赠书 | 【第34期】如何成为一个软件架构师?

点击阅读全文购买

相关文章:

  • 国产首款——飞桨PaddlePaddle深度学习框架介绍
  • “MySQL Analytics Engine”来了
  • 【第35期】数字化转型到底该怎么做?
  • 详解华为12种数据采集技术及应用实践
  • JavaScript 25 岁了!
  • 43种机器学习开源数据集(附地址/调用方法)
  • 中国量子计算原型机“九章”问世,普通人怎样初识量子计算?
  • 中台“不火”了,企业数智转型如何破圈?
  • 一本书读懂“财税RPA”
  • 华为官方出品:首本HMS Core技术解析图书问世
  • 系统学习WebAssembly —— 理论篇
  • Hi~大家好,我是今年的圣诞老人华章妹!
  • 8本书助你了解人民日报“创作大脑”
  • 2020 PostgreSQL中国技术大会盛大开启
  • 【第36期】最强Redis特性导图
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • Angular数据绑定机制
  • C++类的相互关联
  • ES6--对象的扩展
  • JavaScript学习总结——原型
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • PAT A1120
  • React Transition Group -- Transition 组件
  • TCP拥塞控制
  • Vue实战(四)登录/注册页的实现
  • vue中实现单选
  • 每个JavaScript开发人员应阅读的书【1】 - JavaScript: The Good Parts
  • 深入浏览器事件循环的本质
  • 网页视频流m3u8/ts视频下载
  • 小程序01:wepy框架整合iview webapp UI
  • SAP CRM里Lead通过工作流自动创建Opportunity的原理讲解 ...
  • 浅谈sql中的in与not in,exists与not exists的区别
  • ​ssh免密码登录设置及问题总结
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • # 深度解析 Socket 与 WebSocket:原理、区别与应用
  • #pragma预处理命令
  • ${ }的特别功能
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (function(){})()的分步解析
  • (Git) gitignore基础使用
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (动态规划)5. 最长回文子串 java解决
  • (入门自用)--C++--抽象类--多态原理--虚表--1020
  • (三)elasticsearch 源码之启动流程分析
  • (一)u-boot-nand.bin的下载
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • ./configure、make、make install 命令
  • .net core 6 redis操作类
  • .NET Framework杂记
  • .Net IOC框架入门之一 Unity
  • .NET 常见的偏门问题
  • .net 提取注释生成API文档 帮助文档