最近借着项目对 Electron 又进行了一波实践

前言

然而早期是有接触过的,当时在做一个桌宠,但是后来烂尾了 🤣

原因则是因为编译 & 安装后产生的文件太多了

在我看来,桌宠应该是一个插件,即编译产生的文件应该是免安装的单个 .exe 文件

所以用 Electron 写桌宠是不可取的

Start

原本准备采用以往的 Vue2 + Electron8 进行开发,但是在一阵纠结之后选择了 Vue3 + TypeScript + Electron 13 🤗

project init

0️⃣ 全局安装 yarn

npm i yarn -g

1️⃣ 全局安装 Vue 脚手架

npm i @vue/cli -g

2️⃣ 构建 Vue 项目

vue create vue-electron-app

这里我选择第三个,因为默认不会加入TypeScript & Router

3️⃣ 加入 Electron

cd vue-electron-app
vue add electron-builder

4️⃣ 关于 package.json

  • electron:serve - 用于 dev 环境,即启动项目
  • electron:build - 用于项目编译

5️⃣ 关于报错

需要安装指定的 ts-loader

yarn add ts-loader@8.2.0

这里必须指定版本,假如不带版本号直接安装 latest 则依然无法启动项目 😴

不使用 ts 则无此问题

6️⃣ End

Another

也可以参考此处进行构建,命令少且更加简单 🥳

https://github.com/electron-vite/electron-vite-vue

开发时碰到的一些问题

关于 Vuex

使用 Vuex 的目的自然是用来存放一些公共数据,典型的例子则是 Token & UserProfile

但是这里没办法使用 Vuex,原因则是因为各个窗口之间是相互独立的,比如在窗口A中修改了Vuex中的数据,在窗口B中是没有办法拿到的

  • 主进程
  • 各个渲染进程

最终我选择在主进程中对这部分数据进行维护,通过渲染进程 & 主进程之间的通信进行数据读取及修改


我维护了一个对象 windows,用于记录渲染进程产生的每个窗口

(路由的 Name 作为 key , BrowserWindow 作为 value

let windows: any = {};

// ...

let anotherWin: any = new BrowserWindow({
  width: res.width,
  height: res.height,
  ...
})
if (winURL) {
  await anotherWin.loadURL(winURL + res.link);
} else {
  createProtocol("app");
  anotherWin.loadURL(`app://./index.html/#/${res.link}`);
}
windows[res.link] = anotherWin;

先通过 ipcRenderer.send 向主进程发送消息,再通过 ipcRenderer.on 接收来自主进程的消息,并使用 Promise 进行返回

import { ipcRenderer } from "electron";
import router from "@/router";

export const get = (key: string) => {
  ipcRenderer.send("getObjectFromMain", {
    key: key,
    link: router.currentRoute.value.name,
  });
  return new Promise((resolve) => {
    ipcRenderer.on("getObjectCallback", (_, res) => {
      resolve(res);
    });
  });
};

利用路由的 name 向指定的渲染进程发送消息

ipcMain.on("getObjectFromMain", (_, res) => {
  let thisWin: any;
  if (res.link == "main") thisWin = win;
  else thisWin = windows[res.link];
  thisWin.webContents.send(
    "getObjectCallback",
    data[res.key as keyof typeof data]
  );
});

然而 npm 上是有 vuex-electron 存在的,但是一眼 4 years ago 不是很敢用 (〜 ̄△ ̄)〜

不过这里会产生另一个问题,即窗口创建时,Vue生命周期钩子函数 onMounted 部分情况下无法使用

如页面加载时需要从主进程中获取数据,此时在 onMounted 内进行将会产生报错

webContents is undefined

即执行 onMounted 内代码片段时,窗体并未完成创建

Then

可以通过在主线程中监听窗体创建完毕(' did-finish-load '),再由进程间通信通知窗体执行初始化... ,以此来代替 onMounted

# 1.主进程
/**
 * window-init
 *
 * ipcRenderer.on("window-init", () => {})
 * 用于处理渲染进程在创建完成时对主进程缓存数据的获取
 */
anotherWin.webContents.on("did-finish-load", () => {
  let result: any = {
    windowId: res.id,
  };
  // 用于创建时的传值
  result.params = res.params;
  anotherWin.webContents.send("window-init", result);
});



# 2.渲染进程
/**
 * window init
 */
ipcRenderer.on("window-init", (_, res: any) => {
  setWindowId(res.windowId);
});

electron:build

“白屏,开发时正常”

归根结底是路由导致的,编译时需要修改为 hash 模式

  • 开发时 - history: createWebHistory(process.env.BASE_URL)
  • 编译时 - history: createWebHashHistory(process.env.BASE_URL)
// new BrowserWindow
if (winURL) {
  await anotherWin.loadURL(winURL + res.link);
} else {
  createProtocol("app");
  anotherWin.loadURL(`app://./index.html/#/${res.link}`);
}


// ./router/index.ts
const routerSetting = () => {
  if (process.env.IS_ELECTRON) {
    return createWebHashHistory(process.env.BASE_URL);
  } else {
    return createWebHistory(process.env.BASE_URL);
  }
};

const router = createRouter({
  history: routerSetting(),
  routes,
});

End

由 Electron 开发的桌面应用其实还挺多的,最有名的自然是 VSCode,其它的比如说还在内测中的 QQ频道,以及雷神加速器(其中弹框消息用到了 ElementUI v2 的消息组件,还是挺明显的,然而确是个减速器 😕

总之,在技术栈有限,且熟悉 js 的情况下选择 Electron 来开发桌面应用无疑是最好的选择

当然,开发上文中所提到的桌宠之类的项目自然要被排除在外,因项目而异


Ex - ploooosion!