如何创建Electron+Vite+Vue3+ts项目

lxf2023-05-06 00:59:02

创建vite项目

创建命令:

yarn create vite electron-demo --template vue-ts

安装依赖并运行

cd electron-demo
yarn
yarn dev

如何创建Electron+Vite+Vue3+ts项目

添加Electron

安装electron

yarn add --dev electron

package.json中添加命令并且指定electron入口

  "main": "electron/main.js" //入口
  "scripts": {
    "dev": "vite",
    "vite:build": "vue-tsc --noEmit && vite build",//修改
    "preview": "vite preview",
    "electron:dev": "electron ." //添加
  },

新建electron文件夹并添加main.js作为electron入口

// main.js

const { app, BrowserWindow } = require("electron");
const path = require("path");

function createWindow() {
  // 创建浏览器窗口
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });

  // 链接url
  mainWindow.loadURL("http://localhost:3000"); // 端口可能会被占用,后面会解决

  // 是否开启开发者工具
  // mainWindow.webContents.openDevTools()
}

// 这段程序将会在 Electron 结束初始化
// 和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
  createWindow();

  app.on("activate", function () {
    // 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
    // 打开的窗口,那么程序会重新创建一个窗口。
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在
// 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on("window-all-closed", function () {
  if (process.platform !== "darwin") app.quit();
});

electron文件夹中添加preload.js作为预加载脚本

// preload.js

window.addEventListener("DOMContentLoaded", () => {});

配置vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
  base: "./",
  plugins: [vue()],
});

运行

// 先执行
yarn dev
// 再执行
yarn electron:dev

如何创建Electron+Vite+Vue3+ts项目

启动脚本

运行yarn dev之后再运行yarn electorn:dev方式属实不优雅,我们期望用一个命令就能解决.

最开始想用npm-run-all这个库去进行串行执行,但是发现yarn dev之后进程不会退出,导致yarn electorn:dev执行不了

  "scripts": {
    "dev": "vite",
    "vite:build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "electron": "electron .",
    "electron:dev": "npm-run-all -s dev electron" // 串行执行
  },

如何创建Electron+Vite+Vue3+ts项目

编写dev.js

在根目录创建一个scripts文件夹,文件夹中创建dev.mjs

注意:

  • 这里是使用mjs,以便使用import方式引用模块.
  • 也可以把改为dev.js去使用require.
  • es规范下__dirname需要自己手动定义.
// script/dev.mjs

import { createServer } from "vite";
import { spawn } from "child_process";
import path from "path";
const __dirname = path.resolve();
const pkgDir = __dirname;
const binDir = path.resolve(pkgDir, "./node_modules/.bin"); // 获取bin目录
const server = await createServer({
  configFile: path.resolve(__dirname, "./vite.config.ts"), // 引用根目录配置文件
});
const { config } = await server.listen();
console.log(config.server.port); // 获取到了vite开启的端口
server.printUrls(); // 打印服务器地址
let electronProcess = spawn(path.resolve(binDir, "electron.cmd"), ["."]); // 开启子进程

或者使用crossenv库简化代码

yarn add cross-env

// script/dev.mjs

import { createServer } from "vite";
import { spawn } from "child_process";
import path from "path";
import crossEnv from "cross-env";
const __dirname = path.resolve();
const server = await createServer({
  configFile: path.resolve(__dirname, "./vite.config.ts"), // 引用根目录配置文件
});
const { config } = await server.listen();
console.log(config.server.port); // 获取到了vite开启的端口
server.printUrls(); // 打印服务器地址
crossEnv(["electron", "."]); // 创建子进程

或者

// script/dev.mjs

import { createServer } from "vite";
import { spawn } from "child_process";
import electron from "electron"; // 引入
import path from "path";
import crossEnv from "cross-env";
const __dirname = path.resolve();
const server = await createServer({
  configFile: path.resolve(__dirname, "./vite.config.ts"), // 引用根目录配置文件
});
const { config } = await server.listen();
console.log(config.server.port); // 获取到了vite开启的端口
server.printUrls(); // 打印服务器地址
let electronProcess = spawn(electron, ["."]); // 开启子进程

这里我们使用最后一种

修改package.json

  "scripts": {
    "dev": "vite",
    "vite:build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "electron": "electron .",
    "electron:dev": "node ./scripts/dev.mjs" // 修改
  },

主进程的ts支持和HMR

如果我们想要在主进程中使用ts,就需要先将ts转换成js,然后将electron入口设在转换后的文件上.

build和watch

首先在electron文件夹中添加vite.config.ts

import { defineConfig } from "vite";

export default defineConfig({
  root: __dirname,
  build: {
    outDir: "../dist/main",
    emptyOutDir: true,
    minify: process.env.NODE_ENV === "production",
    lib: {
      entry: "main.ts",
      formats: ["cjs"],
      fileName: () => "[name].cjs",
    },
  },
});

scripts/dev.mjs目前代码如下

// scripts/dev.mjs
import { createServer, build } from "vite";
import { spawn } from "child_process";
import path from "path";
import crossEnv from "cross-env";
import electron from "electron";
const __dirname = path.resolve();
const server = await createServer({
  configFile: path.resolve(__dirname, "./vite.config.ts"),
});
const { config } = await server.listen();
server.printUrls();
let electronProcess = null;
await build({
  configFile: path.resolve(__dirname, "./electron/vite.config.ts"),
  mode: "development",
  plugins: [
    {
      name: "electron-preload-watcher",
      writeBundle() {
        server.ws.send({ type: "full-reload" }); // 构建完成时强制刷新页面
      },
    },
  ],
  build: {
    lib: {
      entry: "preload.ts", // 预加载脚本的入口
      formats: ["cjs"],
      fileName: () => "[name].cjs",
    },
    sourcemap: "inline",
    watch: {}, // 监听变动
  },
});
await build({
  configFile: path.resolve(__dirname, "./electron/vite.config.ts"),
  mode: "development",
  plugins: [
    {
      name: "electron-main-watcher",
      writeBundle() {
        const env = Object.assign(process.env, {
          VITE_PORT: config.server.port,
        });
        if (electronProcess) {
          electronProcess.removeAllListeners();
          electronProcess.kill(); // 关闭electron进程
        }
        // 开启新的electron进程
        electronProcess = spawn(electron, ["."]);
        // 关闭electron的时候同时关闭主进程
        electronProcess.once("exit", process.exit);
        electronProcess.stdout.on("data", (data) => {
          const str = data.toString().trim();
          str && console.log(str);
        });
        electronProcess.stderr.on("data", (data) => {
          const str = data.toString().trim();
          str && console.error(str);
        });
      },
    },
  ],
  build: {
    lib: {
      entry: "main.ts",
      formats: ["cjs"],
      fileName: () => "[name].cjs",
    },
    emptyOutDir: false, // 不清空文件夹 避免清除掉preload.cjs
    watch: {}, // 监听变动
  },
});


修改main.ts

// main.ts

const { app, BrowserWindow } = require("electron");
const path = require("path");
const port = process.env["VITE_PORT"];
const NODE_ENV = process.env.NODE_ENV;
function createWindow() {
  // 创建浏览器窗口
  const mainWindow = new BrowserWindow({
    width: 600,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.cjs"),
      webSecurity: false,
    },
  });
  // 链接url
  mainWindow.loadURL(
    NODE_ENV === "development"
      ? `http://localhost:${port}`
      : `file://${path.join(__dirname, "../index.html")}`
  );

  // 是否开启开发者工具
  mainWindow.webContents.openDevTools();
}

// 这段程序将会在 Electron 结束初始化
// 和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
  createWindow();

  app.on("activate", function () {
    // 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
    // 打开的窗口,那么程序会重新创建一个窗口。
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在
// 任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on("window-all-closed", function () {
  if (process.platform !== "darwin") app.quit();
});



打包

添加scripts/build.mjs

import { build } from "vite";
import path from "path";
const __dirname = path.resolve();
await build({
  configFile: path.resolve(__dirname, "./electron/vite.config.ts"),
  mode: "production",
  build: {
    lib: {
      entry: "preload.ts", // 预加载脚本的入口
      formats: ["cjs"],
      fileName: () => "[name].cjs",
    },
    sourcemap: "inline",
  },
});
await build({
  configFile: path.resolve(__dirname, "./electron/vite.config.ts"),
  mode: "production",
  build: {
    lib: {
      entry: "main.ts",
      formats: ["cjs"],
      fileName: () => "[name].cjs",
    },
    emptyOutDir: false, // 不清空文件夹 避免清除掉preload.cjs
  },
});
process.exit();

使用electron-builder进行打包操作

yarn add electron-builder

package.json中添加打包设置

{
  "scripts": {
    "dev": "vite",
    "vite:build": "vue-tsc --noEmit && vite build",
    "electron:build": "electron-builder",
    "build": "node ./scripts/build.mjs && vite build && electron-builder",
    "preview": "vite preview",
    "electron": "electron .",
    "electron:dev": "node ./scripts/dev.mjs"
  },
  "build": {
    "appId": "com.your-website.your-app",
    "productName": "ElectronApp",
    "copyright": "Copyright © 2021 <your-name>",
    "mac": {
      "category": "public.app-category.utilities"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true
    },
    "files": [ // 需要打包的文件
      "dist/**/*", // vite打包后的目录
      "electron/**/*" // electron主进程的目录
    ],
    "directories": {
      "buildResources": "assets",
      "output": "dist_electron" // 打包后的输出目录
    }
  }
}

打包结果

如何创建Electron+Vite+Vue3+ts项目

所有代码均已上传到githubelectron-vite-vue

参考

electron-vite

Vite+Electron快速构建一个VUE3桌面应用