如何從零開始搭建一套Electron+Vue3+Vite2+Ts的桌面客戶端開發框架
theme: channing-cyan
一、前言⚡️
☕️最近接到開發
Electron+Vue3
的需求,由於對於Electron
的開發並不是十分的熟悉,所以決定自己從頭搭建一套開發框架,Vue部分使用Vue3+Vite2+Ts+Pinia+Vue-Router4+Axios+Element-Plus
✔️進行搭建,由於本人日常使用element-admin
、element-plus-admin
等後臺框架進行開發,深受影響,因此Vue部分的結構及風格與其會比較像,可能常使用這套工具的小夥伴會很熟悉.Electron部分使用Electron21版本進行搭建,採用electron-bulider進行打包。☄️本文詳細講解了從初始化專案、搭建
Vite2+Vue3
框架、整合Electron
、最後完成打包的思路以及程式碼,包括一些全域性主題
等方案的探討,持久化快取
的使用等,但還是希望能夠真正幫助到大家,能夠根據步驟搭建起一套開發工具,同時在Electron開發或者是打包過程中確實遇到了許多問題,通過很多次的搜尋,我在文章後半部分詳細說明了一些打包遇到的問題及解決方法,希望能有用❤️。⭐本文從零開始搭建,寫了近萬字,內容較多,大家可以選擇性檢視,如果希望檢視Vue3部分的搭建請檢視前半部分,如果希望檢視Electron部分的搭建請檢視後半部分。此外該專案主要使用了相關技術比較新的版本,如果有檢視程式碼的同學請確保Node版本在16以上。
⭐️github地址:Vue3-Electron21-Vite2
⭐️gitee地址:Vue3-Electron21-Vite2
1、專案思路及目錄結構
1.1 目錄結構
✨我們先來預覽一下專案的目錄結構,我們將
electron
的主程序檔案放在src
下的electrin-main
資料夾中,將vue
專案的頁面路由等檔案放在render
中,同時dist
為vite
打包後文件,dist_electron
為electron
打包後文件。
1.2 整體思路
✨該框架目前只搭建了三個視窗,我們希望客戶端點選客戶端過後,先彈出載入介面視窗,可以進行讀取檔案配置進度等操作,最重要的是可以獲取我們的快取,判斷使用者的登入是否是生效狀態,然後決定展示登入視窗,還是主介面視窗。檔案目錄為三個視窗,分別對應主介面,登入,載入頁面,我們electron的loadrUrl採用多視窗的Hash路由模式對應配置,因此我們的路由檔案也只有三個大模組,LayOut是我們主頁面的元件,跟後臺系統一樣,所有的主介面下的路由選單頁面都將在LayOut中渲染,而我們三個視窗也分別對應這三個路由
export const constantRouterMap: (RouteRecordRaw | RouterCustorm)[] = [
{
path: '/load',
name: 'Load',
hidden: true,
component: () => import('@/render/views/load/index.vue'),
},
{
path: '/',
name: 'Index',
component: LayOut,
children:[
{
path: '/',
name: 'Welcome',
meta: { title: '首頁' },
component: () => import('@/render/views/home/index.vue'),
},
{
path: '/news',
name: 'News',
meta: { title: '新聞' },
component: () => import('@/render/views/news/index.vue'),
}
]
},
{
path: '/login',
name: 'Login',
hidden: true,
component: () => import('@/render/views/login/index.vue'),
}
]
2、該專案主要用到的技術棧:
| 主要技術棧及版本 | 版本 |
| --------- | --- |
|electron| ^21.3.0 |
|vite| ^3.2.3|
|vue| ^3.2.41 |
|vue-router|^4.1.6 |
|pinia| ^2.0.26 |
|axios| ^1.2.0 |
|element-plus| ^2.2.22 |
|sass |^1.56.1 |
| 部分外掛 | 作用 | | --------- | --- | |web-storage-cache| 持久化儲存 | |nodemon| electron視窗熱更新 | |electron-is-dev| 開發生產環境判斷| |electron-window-state|視窗狀態的保持 | |-- |--| |-- |--| |-- |--| |-- |--|
二、搭建Vue3開發框架
使用
Vue3+Vite2+Ts+Pinia+Vue-Router4+Axios+Element-Plus
✔️進行搭建,該章節將會帶領大家從零搭建一套Vue3開發框架,由於本文內容過多,有些部分會簡略介紹,詳細介紹請檢視另外一篇文章✈️:Vue3+Ts+Vite2+Pinia 搭建開發腳手架
1、始化Vite專案
1.1 新建vite2+Ts+vue3專案
npm init vite ✅
輸入自定義專案名稱,同時我們本專案選擇Vue+TypeScript進行開發。按照流程配置後我們成功啟動了vite頁面。
1.2 配置路徑別名及自動補全路徑
(1)配置路徑別名
路徑別名即@
可以在vite.config.js的resolve
中配置
```
import { defineConfig,loadEnv} from 'vite'
import type { UserConfig, ConfigEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path' const root = process.cwd()
function pathResolve(dir: string) { return resolve(root, '.', dir) }
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig=>{
return {
plugins: [
vue(),
],
resolve: {
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.less', '.css'],
alias: [
{
find: /@//,
replacement: ${pathResolve('src')}/
}
]
},
}
})
```
(2)自動補全路徑配置
好多同學建立專案發現使用shiyongle@
符號,但是沒有自動出現檔案路徑供我們選擇,如果自己手寫非常麻煩,本專案我們可以在tsconfig.json
中設定我們的補全路徑,在tsconfig.json
中新增程式碼:
js
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
配置完成後就可以愉快的使用程式碼提示了:
2、配置Scss及全域性主題樣式方案
2.1安裝sass及sass-loader
npm install sass sass-loader -D ✅
(1)在src下新建styles檔案,styles下新建main.scss放置我們的全域性樣式
(2)新建var.scss放置全域性變數,設定兩套變數,為接下來配置全域性主題樣式做準備
(3)新建clear.scss進行全域性樣式的清除
可以自行查詢相關css檔案,這裡就不再貼上
(4)新建index.scss進行檔案出口配置
在main.ts中引入index.scss,並且可以看到頁面中的h1標題的樣式已經生效。
```js import { createApp } from 'vue' import App from './App.vue' // 引入全域性樣式 import '@/styles/index.scss'
// 建立例項 const setupAll = async () => { const app = createApp(App) app.mount('#app') }
setupAll() ```
2.1 配置全域性主題色⭐️
配置全域性主題色的方案有非常多,包括
link標籤動態引入
,CSS變數+類名切換
、CSS變數+動態property
、SCSS + mixin + 類名切換
等方案,根據我們的需求,由於本專案只進行暗黑、白亮模式的切換,所以選擇CSS變數+類名切換
方案,同時參考vue-element-plus-admin主題色方案,我們預留CSS變數+動態property
方案進行區域性主題色的修改。更全主題配置可以參考文章✈️'前端主題切換方案'
(1)CSS變數+類名切換
進行暗黑、白亮模式的切換
在上一部分的styles目錄下我們提前新建了全域性變數檔案var.scss,設定了兩套顏色,白亮主題以及暗黑主題下的顏色變數。
:root {
--theme-bg-color: #fff;
--btn-color: #409eff;
}
.dark{
--theme-bg-color:#293146;
--btn-color: #293146;
}
那麼如何在頁面中使用呢?只需要將變數名加入需要切換顏色的地方,在點選切換事件時,使用document.body.className
設定class名稱,從而將變數值切換為相關class名稱下的變數。
```
```
(2)CSS變數+動態property
進行區域性顏色的切換
主要用來實現頭部,選單欄等顏色的切,可以的參考vue-element-plus-admin
的主題設定。在此處我們簡單使用,程式碼與上部分CSS變數+類名切換
進行暗黑、白亮模式的切換的程式碼是一樣的,只不過進行切換時,我們使用
如下方法,直接設定某個變數顏色,從而達到區域性顏色切換的效果。
const handleChangeTheme=()=>{
document.documentElement.style.setProperty('--theme-bg-color','red')
// bol.value=!bol.value
// if(bol.value){
// document.body.className = 'dark';
// }else{
// document.body.className = '';
// }
}
3、配置狀態管理庫Pinia
☘️ Pinia 是 Vue3最新的以及最流行的狀態管理庫,它允許跨元件/頁面共享狀態。 如果熟悉 Composition API,可以認為他是一個簡單的
export const state = reactive({})
,接下來我們就安裝Pinia,具體使用方法可以參考本人之前寫的一篇文章✈️:Vue3+Ts+Vite2+Pinia 搭建開發腳手架npm install pinia ✅
3.1 在src下新建store資料夾,store下新建index.ts,用來建立Pinia例項
```
import type { App } from 'vue'
import { createPinia } from 'pinia'
const store = createPinia()
export const setupStore = (app: App
export { store } ```
3.2 在store資料夾下新建modules資料夾,用來管理模組,同時我們新建一個app.ts檔案,用來儲存app檔案。
``` // stores/app.ts import { defineStore } from 'pinia' import {useCache} from "@/render/hooks/useCache";
const { wsCache } = useCache()
interface AppState { count:number } export const useAppStore = defineStore('app', { state: ():AppState => { return { count: 0 } }, actions: { increment() { this.count++ }, }, }) ```
3.3 main.ts中引入
``` import { createApp } from 'vue' import App from './App.vue' // 引入全域性樣式 import '@/render/styles/index.scss' // 引入狀態管理 import { setupStore } from '@/render/store'
// 建立例項 const setupAll = async () => { const app = createApp(App) setupStore(app) app.mount('#app') }
setupAll() ```
3.4 頁面中使用
js
import {useAppStore} from '@/render/store/modules/app'
const appStore = useAppStore()
appStore.increment()
const count=computed(()=>{
return appStore.count
})
4、使用WebStorageCache
進行持久化儲存
4.1 什麼是WebStorageCache
❓
WebStorageCache
對HTML5localStorage
和sessionStorage
進行了擴充套件,添加了超時時間,序列化方法。可以直接儲存json物件,同時進行超時時間的設定⏰。 同時我們可以結合Pinia
進行資料的持久化儲存。npm install web-storage-cache --save-dev ✅
4.2 封裝WebStorageCache
為hooks
在src下新建hooks資料夾,用來放置我們後續封裝的hooks,新建useCache.ts⛳️ ``` /* * 配置瀏覽器本地儲存的方式,可直接儲存物件陣列。 /
import WebStorageCache from 'web-storage-cache'
type CacheType = 'sessionStorage' | 'localStorage'
export const useCache = (type: CacheType = 'sessionStorage') => { const wsCache: WebStorageCache = new WebStorageCache({ storage: type })
return { wsCache } } ```
4.3 使用WebStorageCache
js
import {useCache} from '@/render/hooks/useCache'
const {wsCache}=useCache('localStorage')
// 設定快取,第三個引數物件配置的超時事件
wsCache.set('login',true,{exp : 100})
// 獲取快取
wsCache.get('login')
5、配置vue-router4
npm install vue-router@4 ✅
☘️src下新建router資料夾,同時新建index.ts檔案,在views下新建login頁面,方便除錯路由,同時為了接下來適配electron,我們的路由模式需要使用hash模式,否則electron打包後很難通過路由引入對應的頁面,並且我們需要配置路由的baseUrl,可以在根目錄新建環境變數檔案
.env.production
,.env.development
,
production下VITE_BASE_PATH='./'
development下VITE_BASE_PATH='/'
```
import { createRouter, createWebHistory, RouteRecordRaw,createWebHashHistory} from 'vue-router'
import type { App } from 'vue'
type RouterCustorm={
hidden?:boolean
}
export const constantRouterMap: (RouteRecordRaw | RouterCustorm)[] = [
{
path: '/login',
name: 'Login',
hidden: true,
component: () => import('@/views/login/index.vue'),
}
]
const router = createRouter({
history: createWebHashHistory(import.meta.env.VITE_BASE_PATH),
routes: constantRouterMap as RouteRecordRaw[],
scrollBehavior: () => ({ left: 0, top: 0 })
})
export const setupRouter = (app: App在main.ts中使用⛳️:
import { createApp } from 'vue'
import App from './App.vue'
// 引入全域性樣式
import '@/render/styles/index.scss'
// 引入狀態管理
import { setupStore } from '@/render/store'
// 引入路由
import {setupRouter} from "@/render/router";
// 建立例項 const setupAll = async () => { const app = createApp(App) setupStore(app) setupRouter(app) app.mount('#app') }
setupAll() ```
6、配置axios請求
此處請求配置參考了Element-Plus-Admin原始碼,其配置還是比較合理的,後續可以根據自己需求完善請求配置以及請求攔截等內容❤️
⚡️配置請求前,先完善環境變數
.env.production
,.env.development
。
production下: ``` // 環境 NODE_ENV=production
介面字首
VITE_API_BASEPATH='pro'
打包路徑
VITE_BASE_PATH='./'
development下:
// 環境
NODE_ENV=development
介面字首
VITE_API_BASEPATH='dev'
打包路徑
VITE_BASE_PATH='/' ```
npm install axios ✅
6.1新建請求配置檔案
src下新建service目錄,service下新建axios目錄,新建config.js,比較值得注意的是打包生產環境介面字首可以是完整線上地址,涉及到electron打包後請求地址問題。 ``` const config: { base_url: { dev: string pro: string } result_code: number | string default_headers: AxiosHeaders; request_timeout: number } = { /* * api請求基礎路徑 / base_url: { // 開發環境介面字首 dev: '/api',
// 打包生產環境介面字首
pro: 'https://xxxx/api',
},
/* * 介面成功返回狀態碼 / result_code: '0000',
/* * 介面請求超時時間 / request_timeout: 60000,
/* * 預設介面請求型別 * 可選值:application/x-www-form-urlencoded multipart/form-data / default_headers: 'application/json' }
export { config } ```
6.2 新建service.ts檔案:
``` import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, AxiosError } from 'axios'
// @ts-ignore import qs from 'qs'
import { config } from './config'
import { ElMessage } from 'element-plus'
const { result_code, base_url } = config // export const PATH_URL ='/api' // @ts-ignore export const PATH_URL = base_url[import.meta.env.VITE_API_BASEPATH] // 建立axios例項 const service: AxiosInstance = axios.create({ baseURL: PATH_URL, // api 的 base_url timeout: config.request_timeout // 請求超時時間 })
// request攔截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
if (
config.method === 'post' &&
(config.headers as AxiosRequestHeaders)['Content-Type'] ===
'application/x-www-form-urlencoded'
) {
config.data = qs.stringify(config.data)
}
// ;(config.headers as AxiosRequestHeaders)['Token'] = 'test test'
// get引數編碼
if (config.method === 'get' && config.params) {
let url = config.url as string
url += '?'
const keys = Object.keys(config.params)
for (const key of keys) {
if (config.params[key] !== void 0 && config.params[key] !== null) {
url += ${key}=${encodeURIComponent(config.params[key])}&
}
}
url = url.substring(0, url.length - 1)
config.params = {}
config.url = url
}
return config
},
(error: AxiosError) => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response 攔截器
service.interceptors.response.use(
(response: AxiosResponse
export { service } ```
6.3 新建index.ts對請求進行匯出
``` import { service } from './service'
import { config } from './config'
const { default_headers } = config
const request = (option: any) => {
const { url, method, params, data, headersType, responseType } = option
return service({
url: url,
method,
params,
data,
responseType: responseType,
headers: {
'Content-Type': headersType || default_headers
}
})
}
export default {
get:
6.4 請求統一管理
在src下新建api資料夾對請求進行管理,新建login資料夾
新建index.ts:
import request from '@/render/service/axios'
import type { UserType } from './types'
export const loginApi = (data: Partial<UserType>): Promise<IResponse<UserType>> => {
return request.post({ url: '/auth/manage/login/pwd', data })
}
新建types.ts:
```
export type UserLoginType = {
username: string
password: string
}
export type UserType = { username: string password: string role: string roleId: string permissions: string | string[] } ```
6.5 vite.config.ts中配置代理
``` import { defineConfig,loadEnv} from 'vite' import type { UserConfig, ConfigEnv } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path'
const root = process.cwd() function pathResolve(dir: string) { return resolve(root, '.', dir) }
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig=>{ let env = {} as any const isBuild = command === 'build' if (!isBuild) { env = loadEnv((process.argv[3] === '--mode' ? process.argv[4] : process.argv[3]), root) } else { env = loadEnv(mode, root) } return { base: env.VITE_BASE_PATH, server: { port: 4000, proxy: { // 選項寫法 '/api': { target: 'https://xxx/api', changeOrigin: true, rewrite: path => path.replace(/^/api/, '') } }, hmr: { overlay: false }, host: '0.0.0.0' }, plugins: [ vue() ], } }) ```
7、配置Element Plus與Element Plus Icons自動匯入
⚙️Element Plus與Element Plus Icons自動匯入主要是使用官網提供的外掛進行vite的配置,但是Icons自動匯入的使用方式有坑,下文有講解,具體參照官網Element Plus
7.1 安裝
npm install element-plus --save ✅
npm install @element-plus/icons-vue ✅
7.2 自動匯入Element Plus
為了減少我們打包後包的體積,我們希望能夠按需引入,但是每次頁面需要手動匯入很麻煩,因為我們使用自動匯入,用起來與全域性引入一樣方便,但是需要使用額外的外掛來匯入要使用的元件
npm install -D unplugin-vue-components unplugin-auto-import ✅
7.3 自動匯入icons
npm install -D unplugin-icons unplugin-auto-import ✅
注意自動匯入icons的用法官網上並未給予示例,使用ep字首自動註冊,因此是個坑。
❗️ icons用法如下:
<i-ep-SemiSelect></i-ep-SemiSelect>
7.4 vite.config.ts中進行配置
``` import { defineConfig,loadEnv} from 'vite' import type { UserConfig, ConfigEnv } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' // 自動匯入element-plus import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import Icons from 'unplugin-icons/vite' import IconsResolver from 'unplugin-icons/resolver'
const root = process.cwd() function pathResolve(dir: string) { return resolve(root, '.', dir) }
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig=>{ return { plugins: [ vue(), AutoImport({ // Auto import functions from Element Plus, e.g. ElMessage, ElMessageBox... (with style) // 自動匯入 Element Plus 相關函式,如:ElMessage, ElMessageBox... (帶樣式) resolvers: [ ElementPlusResolver(), // Auto import icon components // 自動匯入圖示元件 IconsResolver({ prefix: 'Icon', }), ],
}),
Components({
resolvers: [
// Auto register icon components
// 自動註冊圖示元件
IconsResolver({
enabledCollections: ['ep'],
}),
// Auto register Element Plus components
// 自動匯入 Element Plus 元件
ElementPlusResolver(),
],
}),
Icons({
autoInstall: true,
}),
]
}
}) ```
8、設定全域性ts宣告
根目錄下新建types資料夾,新建global.d.ts,其中Window會在electron主程序與渲染程序通訊中用到。
```
export {}
declare global {
interface Window {
electronAPI?: any;//全域性變數名
}
interface AxiosConfig {
params?: any
data?: any
url?: string
method?: AxiosMethod
headersType?: string
responseType?: AxiosResponseType
}
interface IResponse<T = any> {
code: string
data: T extends any ? T : T & any
}
type AxiosHeaders =
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'multipart/form-data'
}
declare const window: any;
```
同時需要將其配置到tsconfig.json中:
三、整合Electron開發環境
1、檔案思路及整合Electron
首先我們將
electron
的主程序檔案等都放在src下的electron-main
資料夾下,同時將我們之前寫的vue
頁面即渲染程序放到render
下,electron-main
下的main.js
檔案主要進行主程序的配置,modules
檔案下的兩個資料夾管理主程序與渲染程序的通訊,shortcut
進行快捷鍵的操作,tray
資料夾進行托盤的設定,until
放置讀寫檔案工具,windows
管理不同的視窗,我們的需求是能夠有一個初始化載入頁面,一個登入頁面,一個主頁面,因此需要管理三個視窗。
安裝electron 並進行命令配置:
npm i electron -D ✅
使用工具nodemon
配置熱更新命令
npm i nodemon ✅
package.json中配置啟動命令:
"start": "nodemon --exec electron . --watch ./ --ext .js,.html,.scss,.vue,.ts,.css",
同時我們可以使用工具concurrently
配置一鍵同時啟動vite專案以及electron客戶端命令
"scripts": {
"serve": "concurrently "npm run dev" "npm run start" ",
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"start": "nodemon --exec electron . --watch ./ --ext .js,.html,.scss,.vue,.ts,.css",
},
2、main.js檔案 ⭐
在這個檔案中,我們初始化載入了loadWindows即初始化載入介面,同時初始化了我們的通訊監聽時間、快捷鍵即鍵盤事件、托盤,同時配置了我們的客戶端聚焦失去焦點事件,方便我們聚焦時互動。
// @ts-ignore const {InitController} =require('./modules/controller/main.js') const {app,BrowserWindow, Tray, Menu} =require ('electron') const {createMainWindow}=require( './windows/mainWindows.js') const {createLoginWindow}=require( './windows/loginWindows.js') const {createLoadWindow}=require( './windows/loadWindows.js') const {initTray}=require('./tray/index.js') const {initShortCut,unInstallShortCut}=require('./shortcut/index') app.whenReady().then(()=>{ // createMainWindow(BrowserWindow) createLoadWindow(BrowserWindow) app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createLoginWindow(BrowserWindow) }) // 初始化監聽事件 InitController(app) // 初始化托盤 initTray() // 初始化快捷鍵 initShortCut() }) // 除了 macOS 外,當所有視窗都被關閉的時候退出程式。 There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) // 客戶端聚焦 app.on('browser-window-focus',()=>{ // 初始化快捷鍵 initShortCut() console.log('browser-window-focus') }) // 客戶端失去焦點 app.on('browser-window-blur',()=>{ // 初始化快捷鍵 unInstallShortCut() console.log('browser-window-blur') }) app.on('will-quit', () => { // 登出快捷鍵 unInstallShortCut() })
3、視窗的設定 ⭐️
思路:我們希望客戶端點選過後,先彈出載入介面,可以進行讀取檔案配置進度等操作,最重要的是可以獲取我們的快取,判斷使用者的登入是否是生效狀態,然後決定展示登入視窗,還是主介面視窗。檔案目錄為三個視窗,分別對應主介面,登入,載入頁面。
3.1 config.js
config.js主要是配置視窗的打包環境的loadeUrl,開發環境與打包環境是不同的,在打包模組我會進行講解。
js const ACHEME = "file"; const path=require('path') const LOAD_URL=`file://${path.join(__dirname, '../../../dist/index.html')}` module.exports={ ACHEME, LOAD_URL }
3.2 視窗外掛使用
-
使用了
electron-window-state
進行視窗狀態的記憶,比如使用者進行了視窗的拖拽、尺寸大小調整,該外掛可以幫助我們進行記憶,下次開啟時是調整後的狀態,使用方法: ```js //客戶端尺寸位置記憶外掛 const windowStateKeeper = require('electron-window-state'); const createLoadWindow=(BrowserWindow)=>{// 預設視窗尺寸 let mainWindowState = windowStateKeeper({ defaultWidth: 1000, defaultHeight: 800 }); const win = new BrowserWindow({ 'x': mainWindowState.x, 'y': mainWindowState.y, 'width': mainWindowState.width, 'height': mainWindowState.height }) // 管理客戶端尺寸位置記憶外掛 mainWindowState.manage(win);
}
``
- 使用了
electron-is-dev`來進行開發環境還是打包環境的判斷。
js
const isDev = require('electron-is-dev')
const loadWinURL =
isDev? `http://localhost:4000/#/load`
: `${LOAD_URL}#load`;
3.3 完整的一個視窗基本配置(以載入視窗為例)⭕️
本介面我們進行了基礎的頁面配置以及使用了外掛等進行了配置,除了上面兩個外掛的配置,我們還進行了frame設定,取消選單邊框,進行頁面的自定義開發,webPreferences下設定preload,渲染器程序到主程序通訊 定義預載入的介面js的路徑,同時也設定了開發者工具、優雅開啟介面防白屏等,並且loadURL的設定,在開發環境下是我們vite啟動的地址加路由,生產環境稍後模組會講解⏭。
`` // @ts-ignore const { LOAD_URL }=require('./config.js'); const path = require('path') const isDev = require('electron-is-dev') //客戶端尺寸位置記憶外掛 const windowStateKeeper = require('electron-window-state'); const url = require("url"); const loadWinURL = isDev?
http://localhost:4000/#/load:
${LOAD_URL}#load`;
const createLoadWindow=(BrowserWindow)=>{
// 預設視窗尺寸
let mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 800
});
const win = new BrowserWindow({
'x': mainWindowState.x,
'y': mainWindowState.y,
'width': mainWindowState.width,
'height': mainWindowState.height,
focusable:true,
show:false,
frame:false,
resizable:false,
webPreferences: {
webSecurity: false,
nodeIntegration: true,
contextIsolation: true,
// 渲染器程序到主程序通訊 定義預載入的介面js
preload: path.resolve(__dirname, '../modules/preload/load.js')
}
})
// 載入頁面地址 線上內網可切換地址
win.loadURL(`${loadWinURL}`)
// 管理客戶端尺寸位置記憶外掛
mainWindowState.manage(win);
// 開發者工具
win.webContents.openDevTools()
// 優雅開啟介面
win.on('ready-to-show',()=>{
win.show()
})
}
module.exports={ createLoadWindow } ```
4、主程序與渲染程序的通訊 ⭐
4.1 主程序監聽渲染程序的訊息
controller資料夾下的main.js主要設定主程序使用
ipcMain.handle
對渲染程序發來的請求進行監聽處理,同時決定視窗的切換、判斷登入或者展示首頁、螢幕縮小 放大 關閉控制等操作。 ``` // @ts-ignore const {ipcMain,BrowserWindow,shell} =require('electron') const {createMainWindow} = require( '../../windows/mainWindows.js') const {createLoginWindow} = require("../../windows/loginWindows"); const settitle=()=>{ // @ts-ignore ipcMain.handle('on-settitle-event',(event,title)=>{ const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) win.setTitle(title) return '已收到' }) } // 瀏覽器開啟頁面 const openByBrowser=()=>{ // @ts-ignore ipcMain.handle('on-useOpenByBrowser-event',(event,url)=>{ shell.openExternal(url) }) } // 登入 展示首頁 const setlogin=()=>{ // @ts-ignore ipcMain.handle('on-setlogin-event',(event,title)=>{ const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) win.close() createMainWindow(BrowserWindow) return '已經登入' }) } // 載入頁判斷登入或者展示首頁 const isShowLogin=()=>{ // @ts-ignore ipcMain.handle('on-isshowlogin-event',(event,value)=>{ if(value){ setTimeout(()=>{ const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) win.close() createLoginWindow(BrowserWindow) },3000) }else{ const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) win.close() createMainWindow(BrowserWindow) }
// const webContents = event.sender
// const win = BrowserWindow.fromWebContents(webContents)
// win.close()
// createMainWindow(BrowserWindow)
return ''
})
} // 首頁螢幕縮小 放大 關閉控制 const setScreen=()=>{ // @ts-ignore ipcMain.handle('on-setScreen-event',(event,value)=>{ console.log(value) const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) if(value==='miniScreen'){ win.minimize() }else if(value==='fullScreen'){ if(win.isMaximized()){ win.restore() }else{ win.maximize() } }else if(value==='closeScreen'){
}
return ''
})
} const InitController=(app)=>{ settitle(), openByBrowser(), setlogin(), isShowLogin(), setScreen() }
module.exports={ InitController,
} ```
4.2 渲染程序向主程序傳送訊息
由於我們有載入介面、登入介面、首頁三個視窗,即三個渲染程序,因此我們設定三個渲染程序的方法檔案,此處以首頁向主程序傳送頁面縮小放大關閉等訊息,利用
contextBridge
橋樑,將我們的方法掛載到window物件上。 ``` const { contextBridge, ipcRenderer } = require('electron') const setTitle=async (title)=>{ let result= await ipcRenderer.invoke('on-settitle-event', title) } // 瀏覽器開啟頁面 const openByBrowser=(url)=>{ ipcRenderer.invoke('on-useOpenByBrowser-event',url) } // 頁面全屏 縮小 關閉 const setScreen=(value)=>{ ipcRenderer.invoke('on-setScreen-event',value) }
contextBridge.exposeInMainWorld('electronAPI', {
setTitle,
openByBrowser,
setScreen,
ipcRenderer: { ...ipcRenderer, on: ipcRenderer.on.bind(ipcRenderer) }
})
在vue頁面中使用:
```
4.3 主程序向渲染程序傳送訊息(此處以鍵盤事件為例,觸發頁面不同操作)
在之前的資料夾中,我們將三個視窗分開管理,那麼我們如何知道是哪個視窗的鍵盤事件執行了呢,我們需要在初始化三個視窗時,將其掛載到electron的全域性物件global上,如以下 ``` const win = new BrowserWindow({ 'width': 100px, 'height': 200px, })
global.mainWindow=win
我們可以從global上獲取視窗物件,同時使用webContents.send方法向渲染程序傳送訊息
globalShortcut.register('g', () => {
console.log('g')
if(global.mainWindow){
global.mainWindow.webContents.send('on-shortcut-event','g')
}
})
渲染程序使用
ipcRenderer.on進行訊息監聽。
import {ipcRenderer} from 'electron'
ipcRenderer.on('on-shortcut-event',(event:any,data:any)=>{
console.log(event,data)
})
``
正常來說,這樣獲取ipcRenderer直接進行監聽看似非常簡單,但是坑點在於vue頁面中
import {ipcRenderer} from 'electron'`根本無法獲取ipcRenderer❌,因此我查詢了許多文件,發現有一種問題可以解決,就是我們利用渲染程序向主程序傳送訊息時在window身上掛載一個ipcRenderer,一定要bind一下on方法:
js
contextBridge.exposeInMainWorld('electronAPI', {
setTitle,
openByBrowser,
setScreen,
ipcRenderer: { ...ipcRenderer, on: ipcRenderer.on.bind(ipcRenderer) }
})
vue頁面中:
js
if(window.electronAPI && window.electronAPI.ipcRenderer){
window.electronAPI?.ipcRenderer?.on('on-shortcut-event',(event:any,data:any)=>{
console.log(event,data)
})
}
5、鍵盤快捷鍵事件監聽
在shortcut資料夾下新建index.js,註冊鍵盤事件,globalShortcut.register第一個引數為快捷鍵組合,第二個引數為觸發後的回撥,綜合上一步我們可以使用global.mainWindow.webContents.send
向渲染程序vue頁面發訊息,讓其進行不同的操作,同時要記得登出快捷鍵⚠️。在主程序main.js中app失去焦點關閉等狀態時進行登出。
```
const {app, globalShortcut } = require('electron')
const initShortCut=()=>{ // globalShortcut.register('CommandOrControl+X', () => { // console.log('CommandOrControl+X is pressed') // }) globalShortcut.register('g', () => { console.log('g') if(global.mainWindow){ global.mainWindow.webContents.send('on-shortcut-event','g') }
})
}
const unInstallShortCut=()=>{
// 登出快捷鍵
globalShortcut.unregister('CommandOrControl+X')
globalShortcut.unregister('g')
// 登出所有快捷鍵
globalShortcut.unregisterAll()
}
module.exports={
initShortCut,
unInstallShortCut
}
登出快捷鍵:
// 客戶端失去焦點
app.on('browser-window-blur',()=>{
// 初始化快捷鍵
unInstallShortCut()
console.log('browser-window-blur')
})
app.on('will-quit', () => {
// 登出快捷鍵
unInstallShortCut()
})
```
6、托盤設定
什麼是托盤,其實在windows中就是桌面底部工作列右側的小圖示點選右鍵可以進行操作
如何實現呢❓
在tray資料夾下新建index.js,icon可以設定展示圖示,setToolTip可以設定滑鼠放上去的文字提示,contextMenu是右擊出現的操作選單,我們這裡可以操作視窗的退出等等操作。
```js const { app, Menu, Tray,nativeImage } = require('electron') const path=require('path') let appIcon = null const initTray=()=>{ const iconPath = path.join(__dirname, '/icone.ico').replace('/\/g','\'); appIcon = new Tray(nativeImage.createFromPath(iconPath)) appIcon.setToolTip('This is my application.') const contextMenu = Menu.buildFromTemplate([ { label: '退出',type: 'radio', click:()=>{ app.quit() }}, { label: 'Item2', type: 'radio' } ])
// Make a change to the context menu
contextMenu.items[1].checked = false
// Call this again for Linux because we modified the context menu
appIcon.setContextMenu(contextMenu)
}
module.exports={ initTray } ```
7、可能會遇到的安全策略警告的關閉 在html中新增meta
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' ;">
四、打包 ⭕️
打包可能會遇到的問題包括打包後窗口
loadURL
的設定,打包的配置(包括名稱、圖示、包含檔案等),多視窗下的頁面路徑與路由的配置,打包後網路請求的補全等問題,接下來我們就走一遍完整流程,並且對這些問題進行講解❗️。先看一下我打包後的檔案結構,dist是檔案vite打包過後的檔案,dist_electron是electron打包後的檔案,裡邊包含安裝exe檔案登,dist_electron這個目錄是在package.json中配置打包引數directories設定的。
1、使用electron-builder
進行打包
npm i electron-builder -D ✅
配置打包命令('app:dist'):
打包electron需要先打包vite,將vue檔案打包完成後,再進行electron的打包。為了方便我自定了命令electron:dist,可以順序執行vite打包、electron打包
"scripts": {
"serve": "concurrently "npm run dev" "npm run start" ",
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"start": "nodemon --exec electron . --watch ./ --ext .js,.html,.scss,.vue,.ts,.css",
"app:dir": "electron-builder --dir",
"app:dist": "electron-builder",
"electron:dist": "vue-tsc && vite build && electron-builder"
},
2、package.json中配置打包引數
"build": {
"appId": "com.clound.app", // app名稱
"productName": "clound", // 專案桌面名稱 生成的安裝檔名
"win": {
"icon": "./public/icon.ico", // 配置的安裝後桌面上的圖示
"artifactName": "${productName}.${ext}"
},
"directories": {
"output": "dist_electron" // 打包後輸出資料夾名稱
},
"files": ["./dist","./package.json","./src/electron-main"], // 打包時包含的檔案 一定要包含dist 即vite打包後文件
"nsis": {
"oneClick": false, // 是否一鍵安裝,不可更改目錄等選項,預設為true
"allowElevation": true, // 是否允許許可權提升。如果為false,則使用者必須使用提升的許可權重新啟動安裝程式。
"allowToChangeInstallationDirectory": true, // 是否允許更改安裝路徑
"createDesktopShortcut": true, // 是否建立桌面圖示
"createStartMenuShortcut": true, // 建立開始選單圖示
"runAfterFinish": true, // 安裝完成請求執行
"installerIcon": "./public/icon.ico", // 安裝包圖示
"uninstallerIcon": "./public/icon.ico", //解除安裝程式圖示
"installerHeaderIcon": "./public/icon.ico", // 安裝時頭部圖示
"shortcutName": "dclound" // 桌面圖示名稱
}
},
⚡️可能會遇到的問題:
配上我的ico資源路徑以及vite路徑配置,這裡的ico資源配置路徑可能會遇到問題,並且ico不能小於256*256
vite.config.js中的打包路徑配置
```
base: env.VITE_BASE_PATH,
production:VITE_BASE_PATH='./'
development:VITE_BASE_PATH='/' ```
3、electron多視窗loadURL
的處理
在開發環境下我們可以直接使用專案啟動埠以及路由,但是打包後我們會有多個視窗,載入檔案該載入哪裡呢,路由又該如何使用呢?
3.1 設定檔案路徑
其實打包後我們視窗載入的地址應該是我們vite打包後的檔案即dist所在檔案位置,在我的config.js中已經定義好了vite打包後dist檔案的路徑,注意看dist檔案相對於我們當前config.js的文職,決定了../../../dist/index.html
拼接的層級⚠️
const ACHEME = "file";
const path=require('path')
// const LOAD_URL = `${ACHEME}://${ __dirname}/index.html`;
const LOAD_URL=`file://${path.join(__dirname, '../../../dist/index.html')}`
module.exports={
ACHEME,
LOAD_URL
}
視窗中配置loadUrl(isDev是我們之前提到過的判斷開發環境生產環境的工具):
``
const mainWinURL =
isDev ?
http://localhost:4000/#/:
${LOAD_URL}#`;
// 視窗例項 win.loadURL(mainWinURL)
```
3.2 多視窗打包後如何使用路由載入不同頁面
先來看一下我們打包前後三個視窗的loadUrl,可以看到打包後我們可以使用檔案路徑拼接#路由方式找到頁面,但是前提是我們Vue的路由設定必須是hash模式才會生效
// 載入視窗
const loadWinURL =
isDev? `http://localhost:4000/#/load`
: `${LOAD_URL}#load`;
// 主視窗
const mainWinURL =
isDev ? `http://localhost:4000/#/`
: `${LOAD_URL}#`;
// 登入視窗
const loginWinURL =
isDev? `http://localhost:4000/#/login`
: `${LOAD_URL}#login`;
同時要注意不同環境的baseUrl的設定,否則會出現白屏。
4、打包後白屏,source中檢視 無法找到資源情況
首先要檢視package.json中build打包引數 "files": ["./dist","./package.json","./src/electron-main"]
,是否包含了dist即vite打包後的檔案,只有配置了包含,electron打包時才會一起載入打包。
其次如果使用了hash路由載入了不同頁面要看baseUrl是否設定正確:
生產環境下 VITE_BASE_PATH='./'
const router = createRouter({
history: createWebHashHistory(import.meta.env.VITE_BASE_PATH),
routes: constantRouterMap as RouteRecordRaw[],
scrollBehavior: () => ({ left: 0, top: 0 })
})
5、打包後網路請求file://
,並不是想要的http等請求
打包後需要補全我們請求地址baseUrl,如生產環境下這樣補全,可以檢視上方vue框架搭建時我的配置。
const service: AxiosInstance = axios.create({
baseURL: 'https://XXXX/api', // api 的 base_url
timeout: config.request_timeout // 請求超時時間
})
使用https可能會遇到的安全策略警告的關閉 在html中我們之前的meta中新增http://* https://*
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' http://* https://*;">
五、寫在最後 ✍️
這篇文章應該是目前我在掘金寫的最長的一篇文章了,寫了幾個小時,接近萬字,一開始只是想總結一下開發經驗,但是想起自己在開發時遇到的坑點,希望能夠詳細的總結一篇文章,讓後來的同學少開幾個瀏覽器查問題❌, 本文講解了整個框架的思路以及遇到的問題,但是僅僅依靠文章很多問題並不能詳細闡述,建議大家可以拉一下程式碼看看,這樣可能會更好的理解,同時如果有問題歡迎大家來討論❤️。
開啟掘金成長之旅!這是我參與「掘金日新計劃 · 2 月更文挑戰」的第 2 天,點選檢視活動詳情