在现代桌面应用开发领域,Electron凭借其跨平台特性和Web技术栈的优势,成为了众多开发者的首选框架。而electron-vite作为新一代的构建工具,结合了Vite的极速开发体验和Electron的强大能力,为桌面应用开发带来了全新的可能。本文将基于当前的 multimedia-browser 项目,详细介绍如何使用electron-vite从零开始搭建一个桌面应用。
一、electron-vite 简介
1.1 什么是electron-vite
electron-vite 是一个专为 Electron 应用打造的构建工具,它基于 Vite 进行定制化开发,为 Electron 应用提供了极速的开发体验和优化的构建输出。
1.2 核心优势
- 极速开发体验:利用Vite的按需编译和热模块替换(HMR)特性,大幅提升开发效率
- 分离构建:独立构建主进程、预加载脚本和渲染进程,优化构建输出
- 现代Web技术栈:支持 Vue、React 等现代前端框架
- TypeScript支持:内置 TypeScript 支持,提升代码质量和开发体验
- 优化构建输出:针对 Electron 应用特点进行构建优化
二、项目初始化与配置
2.1 初始化项目
使用electron-vite初始化一个新项目非常简单,只需要一个命令即可:
bash
# 使用npm
npm create electron-vite@latest my-app -- --template vue
# 使用yarn
yarn create electron-vite my-app --template vue
# 使用pnpm
pnpm create electron-vite my-app -- --template vue
以当前的multimedia-browser项目为例,我们使用了Vue模板来初始化项目。
2.2 项目结构解析
初始化后的项目结构如下:
bash
├── src/
│ ├── main/ # 主进程代码
│ ├── preload/ # 预加载脚本
│ └── renderer/ # 渲染进程代码
├── electron-vite.config.mjs # electron-vite配置文件
├── package.json # 项目依赖和脚本
└── electron-builder.yml # 打包配置文件
这种结构清晰地分离了Electron的不同进程,便于开发和维护。
2.3 核心配置文件解析
2.3.1 electron-vite.config.mjs
这是electron-vite的核心配置文件,定义了主进程、预加载脚本和渲染进程的构建配置:
javascript
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()] // 将依赖排除在打包外
},
preload: {
plugins: [externalizeDepsPlugin()] // 同样排除依赖
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src') // 定义别名
}
},
plugins: [vue()] // 使用Vue插件
}
})
这个配置文件的关键在于:
- 对主进程和预加载脚本使用
externalizeDepsPlugin
插件,将Node.js依赖排除在打包外 - 对渲染进程配置Vue插件和路径别名,提升开发体验
2.3.2 package.json
package.json文件定义了项目的依赖和脚本命令:
javascript
{
"name": "multimedia-browser",
"version": "1.0.0",
"description": "An Electron application with Vue",
"main": "./out/main/index.js",
"scripts": {
"dev": "electron-vite dev", // 开发模式
"build": "electron-vite build", // 构建应用
"start": "electron-vite preview", // 预览构建结果
"build:win": "npm run build && electron-builder --win", // 构建Windows安装包
"build:mac": "npm run build && electron-builder --mac", // 构建macOS安装包
"build:linux": "npm run build && electron-builder --linux" // 构建Linux安装包
},
"dependencies": {
// 生产依赖
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"electron-updater": "^6.3.9",
// Vue相关依赖
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"pinia": "^3.0.3",
// UI库
"element-plus": "^2.11.2",
"@element-plus/icons-vue": "^2.3.2"
},
"devDependencies": {
// 开发依赖
"electron": "^37.2.3",
"electron-vite": "^4.0.0",
"electron-builder": "^25.1.8",
"vite": "^7.0.5",
"@vitejs/plugin-vue": "^6.0.0"
}
}
三、主进程实现
主进程是Electron应用的核心,负责创建窗口、处理系统事件和管理应用生命周期。在当前项目中,主进程代码位于src/main/index.js
。
3.1 创建窗口
javascript
function createWindow() {
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
// 放宽安全限制以允许加载本地文件
webSecurity: false,
allowRunningInsecureContent: true,
// 启用Node.js集成
nodeIntegration: true,
contextIsolation: false
}
})
// 窗口准备好显示时再显示
mainWindow.on('ready-to-show', () => {
mainWindow.show()
// 在开发模式下自动打开开发者工具
if (is.dev) {
mainWindow.webContents.openDevTools()
}
})
// 加载URL或本地HTML文件
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
3.2 应用生命周期管理
javascript
// 当Electron完成初始化并准备创建浏览器窗口时调用
app.whenReady().then(() => {
// 注册自定义协议处理本地媒体文件
protocol.registerFileProtocol('media-file', (request, callback) => {
const url = request.url.replace('media-file://', '')
const filePath = decodeURIComponent(url)
callback(filePath)
})
// 设置应用用户模型ID
electronApp.setAppUserModelId('com.electron')
// 创建窗口
createWindow()
// 处理macOS特有的激活事件
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// 处理窗口关闭事件
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
3.3 主进程IPC通信实现
主进程通过ipcMain模块监听和处理来自渲染进程的消息:
javascript
// IPC测试
ipcMain.on('ping', () => console.log('pong'))
// 处理打开目录对话框
ipcMain.handle('open-directory-dialog', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: '选择媒体文件目录'
})
return result.canceled ? null : result.filePaths[0]
})
// 处理获取目录下的文件
ipcMain.handle('get-files-in-directory', async (_, directoryPath) => {
try {
const entries = await fs.readdir(directoryPath, { withFileTypes: true })
// 过滤文件类型
const supportedExtensions = {
images: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'],
videos: ['mp4', 'avi', 'mov', 'mkv', 'wmv'],
audio: ['mp3', 'wav', 'ogg', 'flac', 'aac']
}
const files = []
for (const entry of entries) {
if (!entry.isFile()) continue
const fileName = entry.name
const filePath = join(directoryPath, fileName)
// 获取文件信息并分类
const stats = await fs.stat(filePath)
let fileType = 'other'
const extension = fileName.toLowerCase().split('.').pop()
for (const [type, extensions] of Object.entries(supportedExtensions)) {
if (extensions.includes(extension)) {
fileType = type
break
}
}
files.push({
name: fileName,
path: filePath,
size: stats.size,
type: fileType,
modifiedTime: stats.mtimeMs
})
}
return files
} catch (error) {
console.error('读取目录内容失败:', error)
throw error
}
})
四、预加载脚本实现
预加载脚本是连接主进程和渲染进程的桥梁,它可以在渲染进程加载前执行,并有权限访问Node.js API。在当前项目中,预加载脚本位于src/preload/index.js
。
javascript
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// 自定义API,用于渲染进程调用
const api = {
// 打开目录选择对话框
openDirectory: () => {
return electronAPI.ipcRenderer.invoke('open-directory-dialog')
},
// 获取目录下的文件
getFilesInDirectory: (path) => {
return electronAPI.ipcRenderer.invoke('get-files-in-directory', path)
}
}
// 使用contextBridge将API暴露给渲染进程
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', {
...electronAPI,
ipcRenderer: electronAPI.ipcRenderer,
process: {
versions: process.versions
}
})
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// 兼容旧版本Electron
window.electron = electronAPI
window.electron.process = {
versions: process.versions
}
window.api = api
}
五、渲染进程实现
渲染进程负责应用的UI渲染和用户交互,在当前项目中,渲染进程基于Vue 3实现,代码位于src/renderer/src/
目录。
5.1 应用入口文件
javascript
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElIcons from '@element-plus/icons-vue'
import i18n from './i18n'
const app = createApp(App)
const pinia = createPinia()
// 注册所有Element Plus图标
for (const name in ElIcons) {
app.component(name, ElIcons[name])
}
// 初始化主题
import { initializeTheme } from './utils/themeUtils.js'
initializeTheme()
// 安装插件
app.use(router)
app.use(pinia)
app.use(ElementPlus)
app.use(i18n)
// 挂载应用
app.mount('#app')
5.2 路由配置
项目使用Vue Router进行路由管理,配置位于src/renderer/src/router/index.js
:
javascript
import { createRouter, createWebHashHistory } from 'vue-router'
// 定义路由
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@renderer/views/HomeView.vue')
},
{
path: '/media',
name: 'Media',
component: () => import('@renderer/views/MediaView.vue')
},
{
path: '/settings',
name: 'Settings',
component: () => import('@renderer/views/SettingsView.vue')
}
]
// 创建路由器实例
const router = createRouter({
history: createWebHashHistory(), // 使用hash模式路由,适合Electron应用
routes
})
export default router
5.3 状态管理
项目使用Pinia进行状态管理,以媒体模块为例,代码位于src/renderer/src/store/modules/media.js
:
javascript
import { defineStore } from 'pinia'
export const useMediaStore = defineStore('media', {
state: () => ({
// 媒体文件列表
mediaFiles: [],
// 当前选中的媒体文件
selectedFile: null,
// 当前目录路径
currentPath: '',
// 媒体文件筛选器
filter: {
type: 'all', // 'all', 'images', 'videos', 'audio'
search: ''
},
// 媒体库设置
settings: {
viewMode: 'grid', // 'grid', 'list'
sortBy: 'name', // 'name', 'date', 'size'
sortOrder: 'asc' // 'asc', 'desc'
}
}),
getters: {
// 获取筛选后的媒体文件
filteredMediaFiles: (state) => {
let files = [...state.mediaFiles]
// 类型筛选
if (state.filter.type !== 'all') {
files = files.filter((file) => file.type.startsWith(state.filter.type))
}
// 搜索筛选
if (state.filter.search) {
const searchLower = state.filter.search.toLowerCase()
files = files.filter(
(file) =>
file.name.toLowerCase().includes(searchLower) ||
file.path.toLowerCase().includes(searchLower)
)
}
// 排序
files.sort((a, b) => {
let comparison = 0
switch (state.settings.sortBy) {
case 'name':
comparison = a.name.localeCompare(b.name)
break
case 'date':
comparison = a.modifiedTime - b.modifiedTime
break
case 'size':
comparison = a.size - b.size
break
}
return state.settings.sortOrder === 'asc' ? comparison : -comparison
})
return files
}
},
actions: {
// 设置媒体文件列表
setMediaFiles(files) {
this.mediaFiles = files
},
// 添加单个媒体文件(避免重复)
addMediaFile(file) {
const isDuplicate = this.mediaFiles.some(existingFile => existingFile.path === file.path)
if (!isDuplicate) {
this.mediaFiles.push(file)
}
},
// 移除媒体文件
removeMediaFile(filePath) {
this.mediaFiles = this.mediaFiles.filter((file) => file.path !== filePath)
},
// 设置当前选中的文件
setSelectedFile(file) {
this.selectedFile = file
},
// 设置当前目录路径
setCurrentPath(path) {
this.currentPath = path
},
// 更新筛选条件
updateFilter(filter) {
this.filter = { ...this.filter, ...filter }
},
// 更新设置
updateSettings(settings) {
this.settings = { ...this.settings, ...settings }
},
// 清除所有数据
clearAll() {
this.mediaFiles = []
this.selectedFile = null
this.currentPath = ''
}
}
})
5.4 组件实现
以媒体视图组件为例,代码位于src/renderer/src/views/MediaView.vue
,它演示了如何在渲染进程中通过预加载脚本调用主进程的API:
vue
<template>
<div class="media-view">
<el-card class="header-card" shadow="never">
<template #header>
<div class="card-header">
<el-icon><Folder /></el-icon>
<span class="header-title">{{ t('navigation.media') }}</span>
</div>
</template>
<div class="controls">
<el-input
v-model="searchTerm"
:placeholder="t('media.searchPlaceholder')"
prefix-icon="Search"
class="search-input"
@input="handleSearch"
/>
<el-select
v-model="selectedType"
:placeholder="t('media.selectType')"
class="filter-select"
@change="handleTypeFilter"
>
<el-option :label="t('options.allTypes')" value="all" />
<el-option :label="t('media.image')" value="images" />
<el-option :label="t('media.video')" value="videos" />
<el-option :label="t('media.audio')" value="audio" />
</el-select>
<div class="view-controls">
<el-button
:type="viewMode === 'grid' ? 'primary' : 'default'"
icon="Grid"
circle
:title="t('options.gridView')"
@click="setViewMode('grid')"
/>
<el-button
:type="viewMode === 'list' ? 'primary' : 'default'"
icon="List"
circle
:title="t('options.listView')"
@click="setViewMode('list')"
/>
</div>
</div>
</el-card>
<el-card class="path-card mb-4" shadow="never">
<div class="current-path">
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/' }">{{ t('navigation.home') }}</el-breadcrumb-item>
<el-breadcrumb-item>{{ t('navigation.media') }}</el-breadcrumb-item>
<el-breadcrumb-item>{{
currentPath ? currentPath.split('/').pop() : t('media.noPathSelected')
}}</el-breadcrumb-item>
</el-breadcrumb>
<el-button type="primary" :loading="loading" @click="openDirectory">
<el-icon><FolderOpened /></el-icon>
{{ t('media.selectDirectory') }}
</el-button>
</div>
</el-card>
<!-- 其余部分省略 -->
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useMediaStore } from '@renderer/store/modules/media'
import { useI18n } from 'vue-i18n'
const mediaStore = useMediaStore()
const { t } = useI18n()
const loading = ref(false)
const searchTerm = ref('')
const selectedType = ref('all')
const viewMode = computed(() => mediaStore.settings.viewMode)
const filteredMediaFiles = computed(() => mediaStore.filteredMediaFiles)
const selectedFile = computed(() => mediaStore.selectedFile)
const currentPath = computed(() => mediaStore.currentPath)
// 打开目录选择对话框
const openDirectory = async () => {
try {
loading.value = true
// 通过预加载脚本调用主进程API
const directory = await window.api.openDirectory()
if (directory) {
// 获取目录下的文件
const files = await window.api.getFilesInDirectory(directory)
// 更新状态
mediaStore.setMediaFiles(files)
mediaStore.setCurrentPath(directory)
}
} catch (error) {
console.error('打开目录失败:', error)
} finally {
loading.value = false
}
}
// 选择文件
const selectFile = (file) => {
mediaStore.setSelectedFile(file)
}
// 处理搜索
const handleSearch = (value) => {
mediaStore.updateFilter({ search: value })
}
// 处理类型筛选
const handleTypeFilter = (type) => {
mediaStore.updateFilter({ type })
}
// 设置视图模式
const setViewMode = (mode) => {
mediaStore.updateSettings({ viewMode: mode })
}
// 获取预览URL
const getPreviewUrl = (file) => {
// 使用自定义协议加载本地文件
return `media-file://${encodeURIComponent(file.path)}`
}
</script>
5.5 主题系统实现
在现代桌面应用中,主题切换功能是提升用户体验的重要部分。本项目实现了一个完整的主题系统,支持浅色模式、深色模式和跟随系统模式。
主题工具函数位于src/renderer/src/utils/themeUtils.js
:
javascript
// 主题相关工具函数
/**
* 获取当前应使用的主题类名
* @param {string} theme - 主题设置值 ('light', 'dark', 'system')
* @returns {string} - 'light-theme' 或 'dark-theme'
*/
export const getThemeClass = (theme = null) => {
// 如果没有提供主题,则从localStorage获取或使用默认值
const currentTheme = theme || localStorage.getItem('theme') || 'light'
// 跟随系统模式
if (currentTheme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark-theme' : 'light-theme'
}
// 手动选择模式
return currentTheme === 'dark' ? 'dark-theme' : 'light-theme'
}
/**
* 应用主题到根元素
* @param {string} theme - 可选,指定要应用的主题
*/
export const applyTheme = (theme = null) => {
const root = document.documentElement
const themeClass = getThemeClass(theme)
// 移除所有主题类
root.classList.remove('light-theme', 'dark-theme')
// 添加当前主题类
root.classList.add(themeClass)
console.log('应用主题:', themeClass)
}
/**
* 保存主题设置并应用
* @param {string} theme - 要保存的主题值
*/
export const saveAndApplyTheme = (theme) => {
localStorage.setItem('theme', theme)
applyTheme(theme)
}
/**
* 初始化主题(应用启动时使用)
*/
export const initializeTheme = () => {
applyTheme()
}
/**
* 创建系统主题变化的事件监听器
* @param {Function} callback - 当系统主题变化且当前设置为系统模式时触发的回调
* @returns {Function} - 返回一个清理函数,用于移除事件监听器
*/
export const setupSystemThemeListener = (callback = null) => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
// 定义处理函数
const handleSystemThemeChange = () => {
const currentTheme = localStorage.getItem('theme') || 'light'
if (currentTheme === 'system') {
if (callback) {
callback()
} else {
applyTheme()
}
}
}
// 添加事件监听器
mediaQuery.addEventListener('change', handleSystemThemeChange)
// 返回清理函数
return () => {
mediaQuery.removeEventListener('change', handleSystemThemeChange)
}
}
主题系统的使用示例可以在应用的主入口文件中看到:
javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import './assets/styles/main.scss'
import App from './App.vue'
import router from './router'
import i18n from './i18n'
import { initializeTheme } from './utils/themeUtils'
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 初始化主题
initializeTheme()
app.use(createPinia())
app.use(ElementPlus)
app.use(router)
app.use(i18n)
app.mount('#app')
5.6 国际化实现
为了支持全球化用户,本项目使用vue-i18n
实现了完整的国际化功能。国际化配置位于src/renderer/src/i18n/index.js
:
javascript
import { createI18n } from 'vue-i18n'
// 导入语言包
import zhCN from './zh-CN.js'
import zhTW from './zh-TW.js'
import enUS from './en-US.js'
// 获取存储的语言设置,如果没有则使用默认语言
const storedLanguage = localStorage.getItem('language') || 'zh-CN'
// 创建i18n实例
const i18n = createI18n({
legacy: false, // 使用 Composition API 模式
locale: storedLanguage, // 设置当前语言
fallbackLocale: 'zh-CN', // 设置回退语言
messages: {
'zh-CN': zhCN,
'zh-TW': zhTW,
'en-US': enUS
}
})
export default i18n
语言包示例(中文):
javascript
export default {
app: {
name: '多媒体浏览器',
description: '一个基于Electron和Vue.js的跨平台媒体浏览应用'
},
navigation: {
home: '首页',
media: '媒体浏览',
settings: '设置'
},
settings: {
title: '应用设置',
display: '显示设置',
media: '媒体设置',
interface: '界面设置',
about: '关于应用',
defaultViewMode: '默认视图模式',
defaultSortBy: '默认排序方式',
defaultSortOrder: '默认排序顺序',
defaultMediaType: '默认媒体类型',
showFileExtensions: '显示文件扩展名',
showHiddenFiles: '显示隐藏文件',
theme: '主题',
language: '语言',
appName: '应用名称',
version: '版本',
description: '描述',
license: '许可证',
save: '保存设置',
reset: '重置为默认值',
confirmReset: '确定要重置所有设置为默认值吗?',
settingsSaved: '设置已保存!'
},
options: {
gridView: '网格视图',
listView: '列表视图',
sortByName: '按名称排序',
sortByDate: '按日期排序',
sortBySize: '按大小排序',
ascending: '升序',
descending: '降序',
allTypes: '所有类型',
imagesOnly: '仅图片',
videosOnly: '仅视频',
audioOnly: '仅音频',
lightTheme: '浅色模式',
darkTheme: '深色模式',
systemTheme: '跟随系统',
simplifiedChinese: '简体中文',
traditionalChinese: '繁体中文',
english: 'English'
},
media: {
searchPlaceholder: '搜索媒体文件...',
selectType: '选择媒体类型',
image: '图片',
video: '视频',
audio: '音频',
noPathSelected: '未选择路径',
selectDirectory: '选择目录'
}
}
在组件中使用国际化:
vue
<template>
<div class="example">
<h1>{{ t('app.name') }}</h1>
<p>{{ t('app.description') }}</p>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
5.7 设置页面实现
设置页面集成了主题切换、语言选择等功能,是用户自定义应用行为的中心。以下是设置页面的核心实现:
vue
<template>
<div class="settings-view">
<el-card class="header-card" shadow="never">
<template #header>
<div class="card-header">
<el-icon><Setting /></el-icon>
<span class="header-title">{{ t('settings.title') }}</span>
</div>
</template>
</el-card>
<el-card shadow="never" class="mt-4">
<el-form>
<!-- 显示设置部分 -->
<el-form-item label="" prop="section-title">
<el-divider content-position="left">
<el-text type="primary" size="large">
<strong>{{ t('settings.display') }}</strong>
</el-text>
</el-divider>
</el-form-item>
<!-- 视图模式、排序方式等设置项 -->
<el-form-item :label="t('settings.defaultViewMode')">
<el-select v-model="defaultViewMode" style="width: 100%" @change="updateDefaultViewMode">
<el-option :label="t('options.gridView')" value="grid" />
<el-option :label="t('options.listView')" value="list" />
</el-select>
</el-form-item>
<!-- 媒体设置部分 -->
<el-form-item label="" prop="section-title">
<el-divider content-position="left">
<el-text type="primary" size="large">
<strong>{{ t('settings.media') }}</strong>
</el-text>
</el-divider>
</el-form-item>
<!-- 默认媒体类型等设置项 -->
<el-form-item :label="t('settings.defaultMediaType')">
<el-select v-model="defaultMediaType" style="width: 100%" @change="updateDefaultMediaType">
<el-option :label="t('options.allTypes')" value="all" />
<el-option :label="t('options.imagesOnly')" value="images" />
<el-option :label="t('options.videosOnly')" value="videos" />
<el-option :label="t('options.audioOnly')" value="audio" />
</el-select>
</el-form-item>
<!-- 界面设置部分 -->
<el-form-item label="" prop="section-title">
<el-divider content-position="left">
<el-text type="primary" size="large">
<strong>{{ t('settings.interface') }}</strong>
</el-text>
</el-divider>
</el-form-item>
<!-- 主题设置 -->
<el-form-item :label="t('settings.theme')">
<el-select v-model="theme" style="width: 100%" @change="updateTheme">
<el-option :label="t('options.lightTheme')" value="light" />
<el-option :label="t('options.darkTheme')" value="dark" />
<el-option :label="t('options.systemTheme')" value="system" />
</el-select>
</el-form-item>
<!-- 语言设置 -->
<el-form-item :label="t('settings.language')">
<el-select v-model="language" style="width: 100%" @change="updateLanguage">
<el-option :label="t('options.simplifiedChinese')" value="zh-CN" />
<el-option :label="t('options.traditionalChinese')" value="zh-TW" />
<el-option :label="t('options.english')" value="en-US" />
</el-select>
</el-form-item>
<!-- 关于应用部分 -->
<el-form-item label="" prop="section-title">
<el-divider content-position="left">
<el-text type="primary" size="large">
<strong>{{ t('settings.about') }}</strong>
</el-text>
</el-divider>
</el-form-item>
<!-- 应用信息 -->
<el-form-item>
<el-card class="about-card">
<el-descriptions :column="1" border>
<el-descriptions-item :label="t('settings.appName')">{{
t('app.name')
}}</el-descriptions-item>
<el-descriptions-item :label="t('settings.version')">1.0.0</el-descriptions-item>
<el-descriptions-item :label="t('settings.description')">{{
t('app.description')
}}</el-descriptions-item>
<el-descriptions-item :label="t('settings.license')">
MIT License
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮 -->
<div class="actions">
<el-button type="primary" icon="Save" @click="saveSettings">{{
t('settings.save')
}}</el-button>
<el-button type="default" icon="RefreshRight" @click="resetSettings">{{
t('settings.reset')
}}</el-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMediaStore } from '../store/modules/media.js'
import { applyTheme, saveAndApplyTheme, setupSystemThemeListener } from '../utils/themeUtils.js'
const { t, locale } = useI18n()
const mediaStore = useMediaStore()
// 设置状态
const defaultViewMode = ref('grid')
const defaultSortBy = ref('name')
const defaultSortOrder = ref('asc')
const defaultMediaType = ref('all')
const showFileExtensions = ref(false)
const showHiddenFiles = ref(false)
const theme = ref('light')
const language = ref('zh-CN')
// 用于移除事件监听器的清理函数
let cleanupSystemThemeListener = null
// 从store加载设置
const loadSettings = () => {
const storeSettings = mediaStore.settings
defaultViewMode.value = storeSettings.viewMode || 'grid'
defaultSortBy.value = storeSettings.sortBy || 'name'
defaultSortOrder.value = storeSettings.sortOrder || 'asc'
defaultMediaType.value = mediaStore.filter.type || 'all'
// 从localStorage加载其他设置
showFileExtensions.value = localStorage.getItem('showFileExtensions') === 'true'
showHiddenFiles.value = localStorage.getItem('showHiddenFiles') === 'true'
theme.value = localStorage.getItem('theme') || 'light'
language.value = localStorage.getItem('language') || 'zh-CN'
}
// 更新设置的方法
const updateDefaultViewMode = () => {
mediaStore.updateSettings({ viewMode: defaultViewMode.value })
}
const updateDefaultSortBy = () => {
mediaStore.updateSettings({ sortBy: defaultSortBy.value })
}
const updateDefaultSortOrder = () => {
mediaStore.updateSettings({ sortOrder: defaultSortOrder.value })
}
const updateDefaultMediaType = () => {
mediaStore.updateFilter({ type: defaultMediaType.value })
}
const updateShowFileExtensions = () => {
localStorage.setItem('showFileExtensions', showFileExtensions.value)
}
const updateShowHiddenFiles = () => {
localStorage.setItem('showHiddenFiles', showHiddenFiles.value)
}
const updateTheme = () => {
saveAndApplyTheme(theme.value)
}
const updateLanguage = () => {
localStorage.setItem('language', language.value)
// 更新i18n的locale
locale.value = language.value
}
// 保存所有设置
const saveSettings = () => {
// 已经通过update方法保存了大部分设置
// 显示保存成功的提示
alert(t('settings.settingsSaved'))
}
// 重置为默认值
const resetSettings = () => {
if (confirm(t('settings.confirmReset'))) {
// 重置store设置
mediaStore.updateSettings({
viewMode: 'grid',
sortBy: 'name',
sortOrder: 'asc'
})
mediaStore.updateFilter({
type: 'all'
})
// 重置localStorage设置
localStorage.setItem('showFileExtensions', 'false')
localStorage.setItem('showHiddenFiles', 'false')
localStorage.setItem('theme', 'light')
localStorage.setItem('language', 'zh-CN')
// 重新加载设置
loadSettings()
}
}
// 组件挂载时加载设置
onMounted(() => {
loadSettings()
applyTheme(theme.value)
// 设置系统主题变化的事件监听器
cleanupSystemThemeListener = setupSystemThemeListener()
})
// 组件卸载时清理事件监听器
onUnmounted(() => {
if (cleanupSystemThemeListener) {
cleanupSystemThemeListener()
}
})
</script>
<style scoped>
/* 样式部分省略 */
</style>
六、项目构建与打包
6.1 开发模式
在开发模式下,electron-vite 提供了热模块替换功能,可以极大地提升开发效率:
bash
npm run dev
6.2 构建应用
构建生产版本的应用:
bash
npm run build
构建过程会执行以下步骤:
- 清理dist目录
- 使用electron-vite构建主进程、预加载脚本和渲染进程
- 优化资源文件
- 生成可执行文件的基础文件结构
6.3 打包为安装包
electron-vite结合electron-builder可以将应用打包为各个平台的安装包:
bash
# 打包为Windows安装包
npm run build:win
# 打包为macOS安装包
npm run build:mac
# 打包为Linux安装包
npm run build:linux
打包配置在package.json中定义:
json
{
"build": {
"appId": "com.example.multimedia-browser",
"productName": "多媒体浏览器",
"copyright": "Copyright © 2023 example.com",
"directories": {
"output": "dist"
},
"files": [
"dist-electron",
"dist"
],
"win": {
"target": [
"nsis",
"zip"
],
"icon": "public/icon.ico"
},
"mac": {
"target": [
"dmg",
"zip"
],
"icon": "public/icon.icns",
"hardenedRuntime": true,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist"
},
"linux": {
"target": [
"AppImage",
"deb",
"rpm"
],
"icon": "public"
}
}
}
七、electron-vite 项目优化建议
7.1 性能优化
- 代码分割:利用Vite的代码分割功能,减小初始加载体积
- 延迟加载:对非首屏内容使用动态导入进行延迟加载
- 图片优化:使用适当的图片格式和大小,考虑使用WebP等现代格式
- 缓存策略:合理设置缓存策略,提升重复访问性能
- 资源预加载:使用预加载策略加载关键资源,提高应用响应速度
7.2 安全优化
- 内容安全策略(CSP):配置合适的CSP,防止XSS攻击
- 上下文隔离:启用上下文隔离,增强安全性
- 权限控制:最小化应用所需权限
- 依赖管理:定期更新依赖,修复已知漏洞
- 自定义协议:使用自定义协议加载本地文件,替代不安全的file://协议
以下是一个实现自定义协议的示例(在主进程中):
javascript
// 注册自定义协议
protocol.registerFileProtocol('media-file', (request, callback) => {
const url = request.url.replace('media-file://', '')
// 解码URL
const decodedUrl = decodeURIComponent(url)
// 验证路径安全性
if (isSafePath(decodedUrl)) {
callback(decodedUrl)
} else {
callback({ error: -324 })
}
})
// 路径安全性检查函数
function isSafePath(path) {
// 实现路径安全性检查逻辑
// 例如,验证路径是否在允许的目录范围内
return true
}
7.3 用户体验优化
- 启动性能:优化应用启动速度,考虑使用Splash Screen
- 响应性能:优化长时间运行的任务,避免阻塞UI
- 错误处理:添加友好的错误提示和恢复机制
- 主题支持:提供浅色/深色主题支持
- 国际化:支持多语言,满足全球用户需求
- 设置持久化:使用localStorage或其他存储方式保存用户设置
- 键盘快捷键:为常用功能添加键盘快捷键
以下是实现键盘快捷键的示例:
javascript
// 在主进程中注册全局快捷键
globalShortcut.register('CommandOrControl+O', () => {
// 打开文件对话框
dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
}).then(result => {
if (!result.canceled && result.filePaths.length > 0) {
// 发送目录路径到渲染进程
mainWindow.webContents.send('open-directory', result.filePaths[0])
}
})
})
八、总结
electron-vite为Electron应用开发带来了全新的体验,结合了Vite的极速开发体验和Electron的跨平台能力。通过本文的介绍,我们详细了解了如何使用electron-vite从零开始搭建一个桌面应用,包括项目初始化、配置、主进程、预加载脚本和渲染进程的实现,以及如何进行构建和打包。
特别地,我们深入探讨了两个提升现代桌面应用体验的重要功能:主题系统和国际化支持。主题系统通过localStorage保存用户偏好,并支持跟随系统设置的动态切换;国际化则通过vue-i18n实现,支持多语言切换和设置持久化。
此外,我们还学习了如何通过自定义协议安全地加载本地文件、如何实现键盘快捷键提升操作效率,以及如何通过Pinia进行状态管理。
通过electron-vite,开发者可以充分利用现代Web技术栈的优势,同时享受桌面应用的原生体验。electron-vite不仅仅是一个构建工具,它更是一个完整的开发解决方案,可以帮助开发者快速构建高性能、高质量的桌面应用。
随着Web技术和Electron生态的不断发展,electron-vite也在持续演进,为桌面应用开发带来更多可能性和更优秀的开发体验。