最终效果
1.启动项目
1.1 创建一个vue-ts项目
yarn create vite my-vue-app --template vue-ts
1.2安装electron 与打包工具
// 安装成开发依赖,打包后不需要
yarn add electron electron-builder -D
安装一些开发时需要的小工具,后面会详细说明用法
- kill-port:清理端口
- cross-env:设置环境变量
- npm-run-all 顺序执行script脚本
- concurrently 并行执行script脚本
- tsc-watch:编译ts文件,并在文件修改后重新执行编译
- wait-on 等待文件/端口等变化后执行script脚本
yarn add kill-port concurrently cross-env npm-run-all tsc-watch wait-on -D
1.3 启动前配置
修改后的项目完整目录
1. 新建enectron入口文件
新建electron/main.ts
import { app, BrowserWindow} from "electron";
// 创建窗口方法
import { createWindow } from "./utils/createWindow";
app.on("ready", () => {
createWindow(); // 创建窗口
// 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他打开的窗口,那么程序会重新创建一个窗口。
app.on("activate", () => BrowserWindow.getAllWindows().length === 0 && createWindow());
});
// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 macOS窗口全部关闭时,dock中程序不会退出
app.on("window-all-closed", () => {
process.platform !== "darwin" && app.quit();
});
新建electron/utils/createWindow.ts
import { BrowserWindow } from "electron";
import * as path from "path";
/**
* packages.json,script中通过cross-env NODE_ENV=production设置的环境变量
* 'production'|'development'
*/
const NODE_ENV = process.env.NODE_ENV;
/** 创建窗口方法 */
function createWindow() {
// 生成窗口实例
const Window = new BrowserWindow({
minWidth: 1120,
minHeight: 645,
width: 1120, // * 指定启动app时的默认窗口尺寸
height: 645, // * 指定启动app时的默认窗口尺寸
frame: false, // * app边框(包括关闭,全屏,最小化按钮的导航栏) @false: 隐藏
transparent: true, // * app 背景透明
hasShadow: false, // * app 边框阴影
show: false, // 启动窗口时隐藏,直到渲染进程加载完成「ready-to-show 监听事件」 再显示窗口,防止加载时闪烁
resizable: false, // 禁止手动修改窗口尺寸
webPreferences: {
// 加载脚本
preload: path.join(__dirname, "..", "preload")
}
});
// 启动窗口时隐藏,直到渲染进程加载完成「ready-to-show 监听事件」 再显示窗口,防止加载时闪烁
Window.once("ready-to-show", () => {
Window.show(); // 显示窗口
});
// * 主窗口加载外部链接
// 开发环境,加载vite启动的vue项目地址
if (NODE_ENV === "development") Window.loadURL("http://localhost:3920/");
}
// 导出模块
export { createWindow };
新建electron/preload.ts文件(空文件即可)
2. 编写ts声明文件
- 将src/env.d.ts 移动到types/env.d.ts.
- 新建types/electron.d.ts 内容如下
// ? 扩展window对象
interface Window {
/**
* Electron ipcRenderer
* 后面会将进程通讯方法挂载到window对象上,所以添加此接口防止报错
*/
ipc: import("electron").IpcRenderer;
}
3. 修改ts配置文件
1. 编辑tsconfig.json
vue 编译ts的配置文件
{
"compilerOptions": {
// ......
"baseUrl": ".", // 新增
"paths": { // 新增 路径别名
"@/*": ["src/*"],
"~/*": ["types/*"]
}
},
// 修改,识别之前配置的types文件夹下*.d.ts文件
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "types/**/*.d.ts"]
}
2. 新建tsconfig.e.json
electron 编译ts的配置文件
{
// ? 继承tsconfig.json配置 同名配置项会被本配置文件覆盖
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "output/build", // * 编译生成的文件存放路径
"noEmit": false, // * 不生成输出文件
"module": "commonjs", // * 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"baseUrl": ".",
"sourceMap": false, // * 是否生成相应的 '.map' 文件
},
"include": [
"electron"
] // * 指定需要包含的文件 (此处指定electron文件夹下所有文件)
}
4. 修改vite.config.ts
export default defineConfig({
plugins: [vue()],
server: {
strictPort: true, // * 固定端口(如果端口被占用则中止)
host: true, // 0.0.0.0
port: 3920 // 指定启动端口
},
});
5. 修改package.json, 启动项目
- 脚本说明
- 终端运行
yarn start / npm start开始启动项目 start:
- 清理端口3920
(kill-port 3920)然后(&&)并行执行(concurrently -k)命令vite和wait-on tcp:3920 && npm-run-all watch vite:启动vite开发服务器 指定端口3920wait-on tcp:3920 && npm-run-all watch:监听端口3920,连通时执行npm-run-all watch
watch:
- 使用
tsc-watch将ts文件编译成js文件; 通过配置文件tsconfig.e.json指定只编译electron文件夹下的文件,并将js文件输出到output/build文件夹
当ts文件编译完成后(--onSuccess),执行命令npm-run-all start:ect - 启动后tsc-watch将持续监听electron文件夹,每次文件改动都会重新执行编译并运行
(--onSuccess)后的命令
start:ect:
- 设置环境变量
cross-env NODE_ENV=development,electron主进程启动时可以拿到此变量(见electron/utils/createWindow.ts),并据此判断是加载外部链接还是加载打包后文件 - 启动electron app,指定入口文件
./output/build/main.js
{
"name": "test",
"version": "0.0.0",
"license": "MIT",
"scripts": {
+ "start": "kill-port 3920 && concurrently -k \"vite\" \"wait-on tcp:3920 && npm-run-all watch\"",
+ "watch": "tsc-watch --noClear -p tsconfig.e.json --onSuccess \"npm-run-all start:ect\"",
+ "start:ect": "cross-env NODE_ENV=development electron ./output/build/main.js"
},
"dependencies": {
...
},
"devDependencies": {
...
}
}
然后,执行
yarn start / npm start即可启动项目,一个无边框,背景透明的窗口
到此步骤的所有代码在history/item1文件夹下
2.进程通信
2.1基本配置
由于electron文档推行的所谓‘上下文隔离’,所以就不使用remote模块,所有通信方法通过preload.ts进行中转.
todo: 想要在vite+vue项目中更方便的通信,可以使用 @vueuse/electron,这个包可以通过componsitionAPI的方式使用进程通信,像这样
import { useIpcRenderer } from '@vueuse/electron'const ipcRenderer = useIpcRenderer()ipcRenderer.on('custom-event', (event, ...args) => {console.log(args)})编辑electron/preload.ts
import { contextBridge, ipcRenderer } from "electron";
/**
* 通信方法挂载到window对象上
* 在渲染进程中使用:
* <script setup lang="ts">
* window.ipc.on('WindowID', (e, f) => console.log(e, f))
* window.ipc.send('navBar', val)
* </script>
*/
contextBridge.exposeInMainWorld("ipc", {
send: (channel: string, ...args: any[]) => ipcRenderer.send(channel, ...args),
invoke: (channel: string, ...args: any[]): Promise<any> => ipcRenderer.invoke(channel, ...args),
on: (channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {ipcRenderer.on(channel, listener);
},
});
- contextBridge.exposeInMainWorld:将一个对象
ipc:{send:xxx,on:xxx}挂载到window对象上.- 定义一个中转函数ipc.send,渲染进程调用
window.ipc.send('事件名', 参数),ipc.send方法再调用ipcRenderer.send方法并传递参数- 因为在声明文件中添加了
ipc: import("electron").IpcRenderer;,所以也拥有代码提示
也可以用下面的写法,一个函数对应一个通信方法
/**
* 在渲染进程中使用:
* <script setup lang="ts">
* window.ipc.sendNavBar('navBar','close')
* </script>
* 在主进程中接收:
* ipcMain.on('navBar', (event, val) => {
* if (val == 'close') { ......}
* }
*/
const sendNavBar = (channel:string,params:string)=> ipcRenderer.send(channel,params)
contextBridge.exposeInMainWorld("ipc", {
sendNavBar:sendNavBar
});
2.2自定义控制按钮
因为使用了无边框窗口,所以三个控制按钮(最大化,最小化,关闭)需要自定义 这里直接引入antd组件库,后面的右键菜单也需要使用
@vue/compiler-sfc:可以消除安装antd时的控制台警告,不装也没影响
unplugin-vue-components:按需引入组件库插件,也支持elementplus啥的
yarn add ant-design-vue@next @vue/compiler-sfc unplugin-vue-components -D
修改vite.config.ts
......
/**按需导入组件库 查看=>https://zhuanlan.zhihu.com/p/423194571 */
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [AntDesignVueResolver()]
})
],
......
});
在页面上加三个按钮,再随便整点样式
新建electron/utils/navbar.ts
import { BrowserWindow, ipcMain } from "electron";
/**
* @description 进程通讯 渲染进程点击顶部关闭,最小化...按钮时,传递 {val}参数,
* 主进程通过 BrowserWindow.fromWebContents(event.sender)拿到活动窗口的BrowserWindow实例,再通过minimize()等实例方法操作窗口
* @param {Electron.WebContents} event.sender
* @param val {'mini'|'big'|'close'}
* @example
* window.ipc.send('navBar', val) // 渲染进程中
* */
export function onNavbar() {
ipcMain.on('navBar', (event, val) => {
/**
* 通过BrowserWindow.fromWebContents方法拿到window实例
* event.sender 是发送消息的WebContents实例
*/
const window: Electron.BrowserWindow | null = BrowserWindow.fromWebContents(event.sender)
if (val == 'mini') { window?.minimize() } // 最小化窗口
if (val == 'close') { window?.close() } // 关闭窗口
if (val == 'big') { // 全屏/取消全屏
// 因为在createWindow.ts中禁用了修改窗口尺寸(resizable: false),这里先解除
window?.setResizable(true)
window?.isMaximized() ? window?.unmaximize() : window?.maximize();
window?.setResizable(false)
}
})
}
修改electron/main.ts
......
import { onNavbar } from "./utils/navbar"; // 新增
onNavbar(); // 新增
app.on("ready", () => {
......
});
给按钮添加点击事件,向主进程发送消息
<script setup lang="ts">
const navBar = (val: string) => {
window.ipc.send("navBar", val);
};
</script>
<template>
<div style="-webkit-app-region: no-drag">
<a-button @click="navBar('mini')" type="dashed" danger>最小化</a-button>
<a-button @click="navBar('big')" type="dashed" danger>最大化</a-button>
<a-button @click="navBar('close')" type="dashed" danger>关闭</a-button>
</div>
</template>
到此步骤的所有代码在history/item2文件夹下
2.3主进程菜单
2.3.1 创建菜单
通过
Menu.buildFromTemplate方法将一个预定义的对象包装成AppMenu对象,,然后在app启动时(app.on("ready",...)通过Menu.setApplicationMenu方法设置菜单
新建electron/utils/menu.ts
import { Menu } from "electron";
export function createAppMenu() {
const AppMenu: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
// 在mac上,第一个自定义menuItem的label会被应用名覆盖
//此label会被package.json打包配置中的 `build.productName = ‘后台管理’` 覆盖
{ id: "1", label: "App", submenu: [{ id: "1-1", label: "测试" }] },
{
id: "2",
label: "开发",
submenu: [
{ id: "2-1", label: "测试" },
{id: "2-2",label: "检查元素",click(m, window, e){window?.webContents.openDevTools()}}
]
}
];
/** 创建菜单 */
const appMenu = Menu.buildFromTemplate(AppMenu);
return appMenu;
}
编辑electron/main.ts
import { createAppMenu } from "./utils/menu"; // 新增
......
app.on("ready", () => {
// 设置app菜单
Menu.setApplicationMenu(createAppMenu());// 新增
......
});
现在 macos上的app菜单就设置好了
2.4渲染进程菜单(windows菜单)
经过2.3.1的操作,macos上的菜单已经可以显示了,但是由于使用了无边框窗口,win上的菜单随边框一起隐藏了,所以需要自行获取并显示,生成类似vscode的菜单栏
代码写的比较屎,先上个思路,可以自行优化
- 主进程可以通过
Menu.getApplicationMenu()获取app菜单 - app启动时通过进程通信请求菜单,主进程将递归生成的菜单对象发送给渲染进程
- 渲染进程通过a-dropdown等组件,递归生成下拉菜单
- 渲染进程点击菜单项时,发送菜单id,主进程通过
Menu.getApplicationMenu()?.getMenuItemById方法,拿到id对应的菜单对象,并调用click方法
编辑electron/utils/menu.ts,添加
import { Menu, ipcMain, BrowserWindow } from "electron";
interface menuObj {
lable: string;
id: string;
type: string;
child: menuObj[] | null;
}
export function onAppMenu() {
// 渲染进程索取菜单时,如果是windows,返回菜单,如果是macos,返回null
ipcMain.handle("getAppMenu", (): menuObj[] | null => process.platform == "darwin" ? null : getmenu());
ipcMain.on("MenuClick", (event, menuItemId: string) => {
const menuItem = Menu.getApplicationMenu()?.getMenuItemById(menuItemId);
menuItem?.click();
});
}
/**
* @description 递归生成菜单数组,数组传递给渲染进程用于生成windows上左上角菜单栏
* @returns {menuObj} menuArr:{ lable: string, id: string, type: string, child?: menuObj[] }
*/
function getmenu() {
function menu(ims: Electron.MenuItem[]) {
let menuArr: menuObj[] = [];
ims.map((im) => {
let menuObj: menuObj = {
lable: im.label,
id: im.id,
type: im.type,
child: im.type == "submenu" && im.submenu ? menu(im.submenu.items) : null
};
menuArr.push(menuObj);
});
return menuArr;
}
const ims = Menu.getApplicationMenu() as Electron.Menu;
return menu(ims.items);
}
编辑electron/main.ts,
import { onAppMenu, createAppMenu } from "./utils/menu"; // 修改
onAppMenu(); // 新增
......
渲染进程拿到菜单对象,通过a-dropdown等组件就可以生成对应的菜单代码查看
history/item2,这里就不贴了
2.5渲染进程右键菜单
主进程可以通过Menu.buildFromTemplate(contextMenu).popup()为app设置全局右键菜单,但只能是系统默认样式比较丑,所以在渲染进程编写菜单,好处是可以为不同区域设置不同菜单项
新增src/components/contentMenu.vue
antd的a-dropdown右键菜单组件,通过
<slot />传入要包裹的组件,菜单项点击时通过传递的key调用不同的方法,方法内可以只在渲染函数执行,也可以通过进程通信调用主进程方法.可以写多个contentMenu组件,包裹不同的页面,就可以显示多种右键菜单
<script lang="ts" setup>
interface methods {
[propName: string]: Function;
}
const hiddenSidebar = () => console.log("hiddenSidebarfn");
const openDevTool = (key: string) => window.ipc.send("contentMenu", key);
const fullScreen = (key: string) => window.ipc.send("contentMenu", key);
const metnods: methods = {
hiddenSidebar,
openDevTool,
fullScreen
};
const handleClick = ({ key }: { key: string }) => metnods[key] && metnods[key](key);
</script>
<template>
<a-dropdown :trigger="['contextmenu']">
<slot />
<template #overlay>
<a-menu @click="handleClick">
<a-menu-item key="openDevTool">检查元素</a-menu-item>
<a-menu-item key="hiddenSidebar">隐藏/显示边栏</a-menu-item>
<a-menu-item key="fullScreen">进入/退出全屏</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
编辑src/App.vue
<script setup lang="ts">
import ContentMenu from "./components/contentMenu.vue";
......
</script>
<template>
<ContentMenu>
// 用ContentMenu包裹要展示右键菜单的组件
......
</ContentMenu>
</template>
新增electron/utils/contextMenu.ts
import { ipcMain, BrowserWindow } from "electron";
interface methods {
[propName: string]: Function;
}
/**
* @desc: 渲染进程点击自定义菜单项后,通过进程通讯调用主进程方法,实现渲染进程右键菜单
* @param {string } key 如'fullScreen' 传参为methods中键名,
*/
export function onContextMenu() {
ipcMain.on("contentMenu", (event, key: string) => {
methods[key] && methods[key](event);
});
}
// 打开控制台
const openDevTool = (e: Electron.IpcMainEvent) => e.sender.openDevTools();
// 全屏/推出全屏
/**
* 由于electron的某个bug,无边框透明窗口在win上isSetFullScreen总是返回false
* 所以在windows上是否全屏通过在当前窗口实例上挂载变量的方式来判断
*/
type route = extendWindow & Electron.BrowserWindow;
interface extendWindow {
isMax: boolean | null | undefined;
}
const fullScreen = async (e: Electron.IpcMainEvent) => {
const window = BrowserWindow.fromWebContents(e.sender) as route; // 获取窗口实例
const isMac = process.platform == "darwin"; // 判断是否是mac
if (isMac) {
// mac进入/退出简单全屏模式
const isSimpleFS = window.isSimpleFullScreen();
window.setSimpleFullScreen(!isSimpleFS);
} else {
// win进入/退出全屏模式
window.isMax ? window.setFullScreen(false) : window.setFullScreen(true);
window.isMax = !window.isMax;
}
};
const methods: methods = {
openDevTool,
fullScreen
};
修改electron/main.ts
......
import { onContextMenu } from "./utils/contextMenu"; // 新增
onContextMenu();// 新增
......
到此步骤的所有代码在history/item3文件夹下
3.打包项目
打包使用electron-builder 先安装依赖
yarn add electron-builder -D
修改vite.config.ts,打包使用相对路径
export default defineConfig({
base: "./", // * 打包相对路径,否则electron加载index.html时找不到css,js文件
......
});
编辑package.json 说明
- yarn build 开始执行打包脚本
- build:vue 打包vue文件,目录为output/dist
- build:tsc将ts的electron文件编译为js,目录为output/build
- build:all打包两个平台 也可以分别运行build:vue,build:tsc,build:mac来打包指定平台
"main": "output/build/main.js",:是在tsconfig.e.json中指定的,编译ts文件后输出到build目录 electron-builder通过它找到打包入口- preview: 使用electron预览打包后的vue文件
{
......
"main": "output/build/main.js",
"scripts": {
......
"build": "npm-run-all build:vue build:tsc build:all",
"build:vue": "vue-tsc --noEmit && vite build",
"build:tsc": "tsc -p tsconfig.e.json",
"build:all": "electron-builder --mac --windows",
"build:mac": "electron-builder --mac",
"build:win": "electron-builder --windows",
"preview": "cross-env NODE_ENV=production electron ./output/build/main.js"
},
"dependencies": {
"vue": "^3.2.16"
},
"devDependencies": {
......
},
"build": {
"appId": "com.lx000-website.electron-vue3-tpm-test",
"productName": "测试app", //打包后的app名称
"copyright": "Copyright © 2021 <your-name>",
"directories": {
"output": "output/app" // 打包dmg,exe等文件输出目录
},
"win": {
"icon": "public/cccs.icns", // app图标
"target": [ // win打包目标
"nsis", // exe文件
"zip", // 压缩包,解压后可直接运行
"7z"
]
},
// 指定将哪些文件打进最终的安装包
"files": [
// 排除node_modules文件夹
"!node_modules",
"output/dist/**/*",
"output/build/**/*"
],
"mac": {
"category": "public.app-category.utilities.test",
"icon": "public/cccs.icns"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}