最近一直在折騰Vue3+Electron技術結合的實踐,今天就來分享一些vue3.x和electron實現開啟多窗口功能。

開始本文之前,先來介紹下如何使用vue3和electron來快速搭建項目。
目前electron.js的star高達89.3K+,最新穩定版v11.2.3。

使用vue開發electron應用,網上有個比較火的腳手架electron-vue,不過里面的版本太低,而且使用的是vue2.x語法。

今天主要分享的是vue3語法開發electron應用,所以只能手動搭建開發環境。
- 安裝最新Vue CLI腳手架。
npm install -g @vue/cli
- 新建vue3項目
具體的選項配置,大家根據需要勾選。
vue create vue3_electron
- vue3項目中集成electron
安裝vue-cli-plugin-electron-builder插件。
cd vue3_electron vue add electron-builder
之后的安裝過程中會提示安裝electron版本,選擇最新版安裝就行。
- 開發調試/打包構建
npm run electron:serve npm run electron:build

非常簡單,沒有幾步就能快速手動搭建vue3+electron項目,下面就能愉快的開發了。
一般項目中需要新建多開窗口的功能,使用Electron來創建也是非常簡單的。一般的實現方法是 new BrowserWindow({...}) 窗口,傳入參數配置即可快速新建一個窗口。
<body> <button id="aaa">打開A窗口</button> <button id="bbb">打開B窗口</button> <button id="ccc">打開C窗口</button> </body> <script> import path from 'path' import { remote } from 'electron' let BrowserWindow = remote.BrowserWindow; let winA = null; let winB = null; let winC = null; document.getElementById("aaa").onclick = function(){ winA = new BrowserWindow ({width: 1000, height:800}) winA.loadURL("https://aaa.com/"); winA.on("close", function(){ winA = null; }) } document.getElementById("bbb").onclick = function(){ winB = new BrowserWindow ({width: 900, height:650}) winB.loadURL("https://bbb.com/"); winB.on("close", function(){ winB = null; }) } document.getElementById("ccc").onclick = function(){ winC = new BrowserWindow ({width: 500, height:500}) winC.loadURL(`file://${__dirname}/news.html`); winC.on("close", function(){ winC = null; }) } </script>

具體的參數配置,大家可以去查閱文檔,electron官網中都有非常詳細的說明。
https://www.electronjs.org/docs/api/browser-window
但是這種方法每次都要新建一個BrowserWindow,有些挺麻煩的,能不能像下面代碼片段這樣,直接通過一個函數,然后傳入配置參數生成新窗口,顯然是可以的。
windowCreate({ title: '管理頁面', route: '/manage?id=123', width: 750, height: 500, backgroundColor: '#f9f9f9', resizable: false, maximize: true, modal: true, })
background.js配置
'use strict' import { app, BrowserWindow } from 'electron' const isDevelopment = process.env.NODE_ENV !== 'production' import { Window } from './windows' async function createWindow() { let window = new Window() window.listen() window.createWindows({isMainWin: true}) window.createTray() } app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) app.on('ready', async () => { createWindow() }) if (isDevelopment) { if (process.platform === 'win32') { process.on('message', (data) => { if (data === 'graceful-exit') { app.quit() } }) } else { process.on('SIGTERM', () => { app.quit() }) } }
新建一個 window.js 來處理主進程所有的函數。
import { app, BrowserWindow, ipcMain, Menu, Tray } from "electron";
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import path from 'path'
export const windowsCfg = {
id: '', //唯一id
title: '', //窗口標題
width: '', //寬度
height: '', //高度
minWidth: '', //最小寬度
minHeight: '', //最小高度
route: '', // 頁面路由URL '/manage?id=123'
resizable: true, //是否支持調整窗口大小
maximize: false, //是否最大化
backgroundColor:'#eee', //窗口背景色
data: null, //數據
isMultiWindow: false, //是否支持多開窗口 (如果為false,當窗體存在,再次創建不會新建一個窗體 只focus顯示即可,,如果為true,即使窗體存在,也可以新建一個)
isMainWin: false, //是否主窗口(當為true時會替代當前主窗口)
parentId: '', //父窗口id 創建父子窗口 -- 子窗口永遠顯示在父窗口頂部 【父窗口可以操作】
modal: false, //模態窗口 -- 模態窗口是禁用父窗口的子窗口,創建模態窗口必須設置 parent 和 modal 選項 【父窗口不能操作】
}
/**
* 窗口配置
*/
export class Window {
constructor() {
this.main = null; //當前頁
this.group = {}; //窗口組
this.tray = null; //托盤
}
// 窗口配置
winOpts(wh=[]) {
return {
width: wh[0],
height: wh[1],
backgroundColor: '#f00',
autoHideMenuBar: true,
titleBarStyle: "hidden",
resizable: true,
minimizable: true,
maximizable: true,
frame: false,
show: false,
webPreferences: {
contextIsolation: false, //上下文隔離
// nodeIntegration: true, //啟用Node集成(是否完整的支持 node)
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
// devTools: false,
webSecurity: false,
enableRemoteModule: true, //是否啟用遠程模塊(即在渲染進程頁面使用remote)
}
}
}
// 獲取窗口
getWindow(id) {
return BrowserWindow.fromId(id)
}
// 獲取全部窗口
getAllWindows() {
return BrowserWindow.getAllWindows()
}
// 創建窗口
createWindows(options) {
console.log('------------開始創建窗口...')
console.log(options)
let args = Object.assign({}, windowsCfg, options)
console.log(args)
// 判斷窗口是否存在
for(let i in this.group) {
if(this.getWindow(Number(i)) && this.group[i].route === args.route && !this.group[i].isMultiWindow) {
this.getWindow(Number(i)).focus()
return
}
}
let opt = this.winOpts([args.width || 800, args.height || 600])
if(args.parentId) {
console.log('parentId:' + args.parentId)
opt.parent = this.getWindow(args.parentId)
} else if(this.main) {
console.log(666)
}
if(typeof args.modal === 'boolean') opt.modal = args.modal
if(typeof args.resizable === 'boolean') opt.resizable = args.resizable
if(args.backgroundColor) opt.backgroundColor = args.backgroundColor
if(args.minWidth) opt.minWidth = args.minWidth
if(args.minHeight) opt.minHeight = args.minHeight
console.log(opt)
let win = new BrowserWindow(opt)
console.log('窗口id:' + win.id)
this.group[win.id] = {
route: args.route,
isMultiWindow: args.isMultiWindow,
}
// 是否最大化
if(args.maximize && args.resizable) {
win.maximize()
}
// 是否主窗口
if(args.isMainWin) {
if(this.main) {
console.log('主窗口存在')
delete this.group[this.main.id]
this.main.close()
}
this.main = win
}
args.id = win.id
win.on('close', () => win.setOpacity(0))
// 打開網址(加載頁面)
/**
* 開發環境: http://localhost:8080
* 正式環境: app://./index.html
*/
let winURL
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
// win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
winURL = args.route ? `http://localhost:8080${args.route}` : `http://localhost:8080`
// 打開開發者調試工具
// if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
// win.loadURL('app://./index.html')
winURL = args.route ? `app://./index.html${args.route}` : `app://./index.html`
}
win.loadURL(winURL)
win.once('ready-to-show', () => {
win.show()
})
// 屏蔽窗口菜單(-webkit-app-region: drag)
win.hookWindowMessage(278, function(e){
win.setEnabled(false)
setTimeout(() => {
win.setEnabled(true)
}, 100)
return true
})
}
// 關閉所有窗口
closeAllWindow() {
for(let i in this.group) {
if(this.group[i]) {
if(this.getWindow(Number(i))) {
this.getWindow(Number(i)).close()
} else {
console.log('---------------------------')
app.quit()
}
}
}
}
// 創建托盤
createTray() {
console.log('創建托盤')
const contextMenu = Menu.buildFromTemplate([
{
label: '顯示',
click: () => {
for(let i in this.group) {
if(this.group[i]) {
// this.getWindow(Number(i)).show()
let win = this.getWindow(Number(i))
if(!win) return
if(win.isMinimized()) win.restore()
win.show()
}
}
}
}, {
label: '退出',
click: () => {
app.quit()
}
}
])
console.log(__dirname)
const trayIco = path.join(__dirname, '../static/logo.png')
console.log(trayIco)
this.tray = new Tray(trayIco)
this.tray.setContextMenu(contextMenu)
this.tray.setToolTip(app.name)
}
// 開啟監聽
listen() {
// 關閉
ipcMain.on('window-closed', (event, winId) => {
if(winId) {
this.getWindow(Number(winId)).close()
if(this.group[Number(winId)]) delete this.group[Number(winId)]
} else {
this.closeAllWindow()
}
})
// 隱藏
ipcMain.on('window-hide', (event, winId) => {
if(winId) {
this.getWindow(Number(winId)).hide()
} else {
for(let i in this.group) if(this.group[i]) this.getWindow(Number(i)).hide()
}
})
// 顯示
ipcMain.on('window-show', (event, winId) => {
if(winId) {
this.getWindow(Number(winId)).show()
} else {
for(let i in this.group) if(this.group[i]) this.getWindow(Number(i)).show()
}
})
// 最小化
ipcMain.on('window-mini', (event, winId) => {
if(winId) {
this.getWindow(Number(winId)).minimize()
} else {
for(let i in this.group) if(this.group[i]) this.getWindow(Number(i)).minimize()
}
})
// 最大化
ipcMain.on('window-max', (event, winId) => {
if(winId) {
this.getWindow(Number(winId)).maximize()
} else {
for(let i in this.group) if(this.group[i]) this.getWindow(Number(i)).maximize()
}
})
// 最大化/最小化
ipcMain.on('window-max-min-size', (event, winId) => {
if(winId) {
if(this.getWindow(winId).isMaximized()) {
this.getWindow(winId).unmaximize()
}else {
this.getWindow(winId).maximize()
}
}
})
// 還原
ipcMain.on('window-restore', (event, winId) => {
if(winId) {
this.getWindow(Number(winId)).restore()
} else {
for(let i in this.group) if(this.group[i]) this.getWindow(Number(i)).restore()
}
})
// 重新加載
ipcMain.on('window-reload', (event, winId) => {
if(winId) {
this.getWindow(Number(winId)).reload()
} else {
for(let i in this.group) if(this.group[i]) this.getWindow(Number(i)).reload()
}
})
// 創建窗口
ipcMain.on('window-new', (event, args) => this.createWindows(args))
}
}
然后新建一個 plugins.js 文件,用來封裝一些調用方法。
/** * 創建窗口 */ export function windowCreate(args) { console.log(args) ipcRenderer.send('window-new', args) } /** * 關閉窗口 */ export function windowClose(id) { console.log('窗口id:' + id) ipcRenderer.send('window-closed', id) } // ...
在.vue頁面引入plugins.js,執行創建窗口方法。
<template> <div class="app"> <h1>This is an new page</h1> <img src="@assets/v3-logo.png" width="100" alt=""> <p class="mt-10"><a-button type="primary" @click="handleNewWin1">新窗口1</a-button></p> <p class="mt-10"><a-button type="primary" @click="handleNewWin2">新窗口2</a-button></p> </div> </template> <script> import {windowClose, windowCreate} from '@/plugins' export default { setup() { const handleNewWin1 = () => { windowCreate({ title: '管理頁面', route: '/manage', width: 1000, height: 750, backgroundColor: '#f9f9f9', resizable: true, modal: true, maximize: true, }) } const handleNewWin2 = () => { windowCreate({ title: '關於頁面', route: '/about?id=13', width: 750, height: 450, backgroundColor: '#f90', resizable: false, }) } ... return { handleNewWin1, handleNewWin2, ... } } } </script>
- 一些小踩坑記錄
1、創建托盤圖標,一開始圖標一直不顯示,以為__dirname指向src目錄,最后調試發現原來是指向dist_electron
console.log(__dirname) let trayIco = path.join(__dirname, '../static/logo.png') console.log(trayIco)

let trayIco = path.join(__dirname, '../static/logo.png') let tray = new Tray(trayIco)

這樣就能加載本地圖片,顯示托盤圖標了。

2、設置自定義拖拽 -webkit-app-region: drag 無法響應點擊事件,無法禁用右鍵菜單。
當窗體設置frame: false后,可以通過-webkit-app-region: drag來實現自定義拖動,設置后的拖動區域無法響應其它事件,可以給需要點擊的鏈接或者按鈕設置-webkit-app-region: no-drag來實現。

不過還有一個問題,設置-webkit-app-region: drag后,會出現如下圖系統右鍵菜單。

最后經過一番調試,可以通過下面這個方法簡單禁用掉,親測有效~~

win.hookWindowMessage(278, function(e){ win.setEnabled(false) setTimeout(() => { win.setEnabled(true) }, 150) return true })
ok,以上就是基於vue3.x+electron實現多開窗口的思路,希望對大家有些幫助!😃
最后附上一個vite2開發的vue3+vant3.x短視頻實例項目
https://www.cnblogs.com/xiaoyan2017/p/14361160.html

