前言
每次电脑重启后都需要自己再打开一堆的文档、应用程序。总是觉得很麻烦。正巧再学习Electron,就有了一个自己弄一个应用程序管理器,把那些需要启动的命令管理起来。然后就可以直接用客户端直接一键启动那些软件。
技术基础要求
简单的理解electron中的模块分层和知道electron的文件目录结构
一些简单node.js和vue3的相关基础
效果图
实现原理
关键的两个方法是node中的child_process模块执行cmd命令行和使用Electron中的shell模块中的openPath(path)方法去启动应用程序,当然如果你熟悉shell脚本可以都使用child_process去实现
技术栈
| 技术栈 | 官网 |
|---|---|
| vue3 | cn.vuejs.org/ |
| electron | electron.nodejs.cn/ |
| node.js | www.nodeapp.cn/ |
代码实现
一、数据字段与页面设计
// 主要数据格式
type FileData = {
id?:String,
url:String, // 启动文件的路径或者是命令行命令
startMode:Boolean, // 启动模式,T:启动应用,F:命令行执行
editStatus:Boolean, // 开启输入框编辑
}
页面的html样式
<template>
<div v-for=" (fileData, index) in startFileData" :key="index">
<el-switch v-model="fileData.startMode" size="large" active-text="启动程序" inactive-text="执行命令" />
<el-input :disabled="!fileData.editStatus" v-model="fileData.url" style="width: 300px" placeholder="请输入路径或者命令...">
<template #suffix>
<el-icon class="el-input__icon mouse-style" @click="fileData.editStatus = !fileData.editStatus">
<el-tooltip :disabled="disabled" content="编辑" placement="bottom" :hide-after=0 effect="light">
<Edit v-show="!fileData.editStatus" />
</el-tooltip>
<el-tooltip :disabled="disabled" content="保存" placement="bottom" :hide-after=0 effect="light">
<Select v-show="fileData.editStatus" @click="saveStartFile(fileData)" />
</el-tooltip>
</el-icon>
</template>
</el-input>
<el-button type="primary" v-model="fileData.startMode" @click="checkedFile(fileData)">{{ fileData.startMode ? "选择文件"
: "选择文件夹" }}</el-button>
<el-button type="primary" @click="ls(fileData)">启动</el-button>
<el-button type="danger" @click="delStartFile(fileData)" :icon="Delete" circle />
</div>
<el-button @click="addStartFile">添加</el-button>
</template>
二、ipc通讯模块和获取文件路径的方法配置
选择文件采用的是electron中的dialog.showOpenDialog方法。可以直接获取文件的路径。
但是因为在electron中页面是在渲染器中,而dialog模块是只能在进程中才可以使用的,所以还需要采用electron中的ipc通讯模块
配置ipc的通讯,编写统一监听的方法,可以统一的管理需要监听开启的方法
// mian/monitor/index.ts
interface monitorFunType {
name: string;
args?: any[];
}
/**
* 开启模块的监听方法
* @param monitorModule 需要开启监听的模块
* @param monitorFun 模块中的方法
* @return void
* @example
* onMonitorFunction('monitorModule',"getFilePath")
* onMonitorFunction('monitorModule',["getFilePath","consoleTest"])
* onMonitorFunction('monitorModule',["getFilePath",{name:'monitorModule',args:any}])
*/
export function onMonitorFunction(monitorModule: Record<string, Function>, monitorFun: string | monitorFunType | Array<string | monitorFunType>):void {
if (typeof monitorModule !== 'object' || monitorModule === null) {
throw new Error('需要开启的对象不是一个有效的module');
}
const funcDescriptors = Array.isArray(monitorFun) ? monitorFun : [monitorFun];
funcDescriptors.forEach(descriptor => {
if (typeof descriptor === 'string') {
const methodName = descriptor;
const method = monitorModule[methodName];
if (typeof method === 'function') {
try {
method();
} catch (error:any) {
console.error(`Error calling method ${methodName}: ${error.message}`);
}
} else {
console.warn(`Method ${methodName} does not exist in monitorModule`);
}
} else if (typeof descriptor === 'object' && 'name' in descriptor) {
const { name, args } = descriptor as monitorFunType;
const method = monitorModule[name];
if (typeof method === 'function') {
try {
method(...(args || []));
} catch (error:any) {
console.error(`Error calling method ${name} with args ${JSON.stringify(args || [])}: ${error.message}`);
}
} else {
console.warn(`Method ${name} does not exist in monitorModule`);
}
} else {
throw new Error('启动的方法描述符必须是字符串或者包含name属性的对象');
}
})
}
将一个类别的相关ipc监听模块放在同一个文件里,方便处理 这个文件是处理文件类ipc模块的方法
//main/monitor/fileMonitor.ts
/**
* 接收一个参数,判断作为是否是打开文件夹,还是打开文件。返回文件的绝对路径
* @param {boolena} isFile
* @return filePaths
* @example getFilePath();
*/
export function getFilePath() {
ipcMain.handle("getFilePath", async (event,isFile) => {
let filePaths:any;
if(isFile){
//在showOpenDialog中接收properties的参数中,若是同时存在'openFile'和'openDirectory'时
// 会优先的执行打开文件夹得参数也就是openDirectory,所以需要进行判断
filePaths = (await dialog.showOpenDialog({ properties: ['openFile'] })).filePaths;
}else{
filePaths = (await dialog.showOpenDialog({ properties: ["openDirectory"] })).filePaths;
}
return filePaths.length > 0 ? filePaths[0] : null;
})
}
在主进程中得入口文件中使用监听模块管理方法开启文件管理的ipc监听
//main/index.ts
//引入相关的文件
// 监听方法启动器
import { onMonitorFunction } from './monitor/index'
// 文件监听模块
import * as FileModule from './monitor/fileMonitor'
在创建窗口方法中进行配置监听
function createWindow(): void {
···
// 加载监听器
onMonitorFunction(FileModule, ["getFilePath"])
···
}
然后在渲染器中使用获取文件路径的方法
//renderer/view/index.vue
//获取文件路径
const checkedFile = (fileData) => {
fileData.editStatus = true
ipcRenderer.invoke("getFilePath", fileData.startMode).then(res => {
fileData.url = res
});
}
三、启动应用程序的方法
//在main/monitor/fileMonitor.ts文件配置打开文件的方法
export function openFile(){
ipcMain.handle("openFile",async (event,url)=>{
// 采用shell模块的openPath方法就可以打开文件
// ps:shell模块还有很多相关的操作文件的方法,具体的可以去官网看看
shell.openPath(url)
})
}
// 入口文件中添加监听方法
function createWindow(): void {
···
// 加载监听器
onMonitorFunction(FileModule, ["getFilePath"])
···
}
四、配置执行命令行的方法
child_process直接使用require在渲染器中引入就可以了
命令行中 /k 代表打开命令行后关闭,如果不带 /k的话命令会直接执行完成之后就关闭了命令行
// renderer/src/view/Index.vue
let exec = require('child_process').exec;
function startCommand(url){
let multiLineCommand = `start cmd.exe /k "${url}"`
exec(multiLineCommand, function (error: any, stdout: any, stderr: any) {
if (error) {
console.log(error.stack);
}
console.log('Child Process STDOUT: ' + stdout + stderr);
});
}
五、相关文件的整体代码
rednderer/src/view/index.vue
<template>
<div v-for=" (fileData, index) in startFileData" :key="index">
<el-switch v-model="fileData.startMode" size="large" active-text="启动程序" inactive-text="执行命令" />
<el-input :disabled="!fileData.editStatus" v-model="fileData.url" style="width: 300px" placeholder="请输入路径或者命令...">
<template #suffix>
<el-icon class="el-input__icon mouse-style" @click="fileData.editStatus = !fileData.editStatus">
<el-tooltip :disabled="disabled" content="编辑" placement="bottom" :hide-after=0 effect="light">
<Edit v-show="!fileData.editStatus" />
</el-tooltip>
<el-tooltip :disabled="disabled" content="保存" placement="bottom" :hide-after=0 effect="light">
<Select v-show="fileData.editStatus" @click="saveStartFile(fileData)" />
</el-tooltip>
</el-icon>
</template>
</el-input>
<el-button type="primary" v-model="fileData.startMode" @click="checkedFile(fileData)">{{ fileData.startMode ? "选择文件"
: "选择文件夹" }}</el-button>
<el-button type="primary" @click="ls(fileData)">启动</el-button>
<el-button type="danger" @click="delStartFile(fileData)" :icon="Delete" circle />
</div>
<el-button @click="addStartFile">添加</el-button>
</template>
<script setup lang="ts">
import { onMounted, ref,Ref } from 'vue'
import { ipcRenderer } from 'electron'
import { Edit, Select,Delete } from "@element-plus/icons-vue";
import { saveStartFileApi,getStartFileApi, updateStartFileApi, delStartFileApi } from '@renderer/api/fileApi';
type FileData = {
id?:String,
url:String,
startMode:Boolean,
editStatus:Boolean,
isAdd?:Boolean
}
const disabled = ref(false)
const startFileData:Ref<Array<FileData>> = ref([{
url: '',
startMode: true,
editStatus: false,
isAdd:true
}])
const saveStartFile = (fileData:FileData)=>{
fileData.editStatus = false
if(fileData.isAdd){
saveStartFileApi(fileData).then(res=>{
console.log(res)
getStartFileList(page)
})
}else{
updateStartFileApi(fileData).then(res=>{
console.log(res)
getStartFileList(page)
})
}
}
const page = {current:'1',size:'10'}
const getStartFileList = (page)=>{
getStartFileApi(page).then(res=>{
startFileData.value = res.data.records
},err=>{
console.log(err)
})
}
const delStartFile = (fileData)=>{
delStartFileApi(fileData).then(res=>{
console.log(res)
})
}
const addStartFile = ()=>{
startFileData.value.push({
url:'',
startMode:true,
editStatus:true,
isAdd:true
})
}
const checkedFile = (fileData) => {
fileData.editStatus = true
ipcRenderer.invoke("getFilePath", fileData.startMode).then(res => {
fileData.url = res
});
}
let exec = require('child_process').exec;
function ls(fileData) {
const { startMode, url } = fileData
let multiLineCommand: string;
if (startMode) {
ipcRenderer.invoke("openFile", url).then(res => {
console.log(res)
}, err => {
console.log(err)
})
} else {
multiLineCommand = `start cmd.exe /k "${url}"`
exec(multiLineCommand, function (error: any, stdout: any, stderr: any) {
if (error) {
console.log(error.stack);
}
console.log('Child Process STDOUT: ' + stdout + stderr);
});
}
}
onMounted(()=>{
getStartFileList(page)
})
</script>
<style lang="scss" scoped>
.el-input__icon {
cursor: pointer !important;
z-index: 1000 !important;
}
</style>
main/monitor/fileMonitor.ts
import { dialog, ipcMain, shell } from 'electron';
/**
* 返回文件的绝对路径
* @param {boolena} isFile
* @return filePaths
* @example getFilePath();
*/
export function getFilePath() {
ipcMain.handle("getFilePath", async (event,isFile) => {
let filePaths:any;
//在showOpenDialog中接收properties的参数中,若是同时存在'openFile'和'openDirectory'时
// 会优先的执行打开文件夹得参数也就是openDirectory,所以需要进行判断
if(isFile){
filePaths = (await dialog.showOpenDialog({ properties: ['openFile'] })).filePaths;
}else{
filePaths = (await dialog.showOpenDialog({ properties: ["openDirectory"] })).filePaths;
}
return filePaths.length > 0 ? filePaths[0] : null;
})
}
export function openFile(){
ipcMain.handle("openFile",async (event,url)=>{
shell.openPath(url)
})
}
main/monitor/index.ts
interface monitorFunType {
name: string;
args?: any[];
}
/**
* 开启模块的监听方法
* @param monitorModule 需要开启监听的模块
* @param monitorFun 模块中的方法
* @return void
* @example
* onMonitorFunction('monitorModule',"getFilePath")
* onMonitorFunction('monitorModule',["getFilePath","consoleTest"])
* onMonitorFunction('monitorModule',["getFilePath",{name:'monitorModule',args:any}])
*/
export function onMonitorFunction(monitorModule: Record<string, Function>, monitorFun: string | monitorFunType | Array<string | monitorFunType>):void {
if (typeof monitorModule !== 'object' || monitorModule === null) {
throw new Error('需要开启的对象不是一个有效的module');
}
const funcDescriptors = Array.isArray(monitorFun) ? monitorFun : [monitorFun];
funcDescriptors.forEach(descriptor => {
if (typeof descriptor === 'string') {
const methodName = descriptor;
const method = monitorModule[methodName];
if (typeof method === 'function') {
try {
method();
} catch (error:any) {
console.error(`Error calling method ${methodName}: ${error.message}`);
}
} else {
console.warn(`Method ${methodName} does not exist in monitorModule`);
}
} else if (typeof descriptor === 'object' && 'name' in descriptor) {
const { name, args } = descriptor as monitorFunType;
const method = monitorModule[name];
if (typeof method === 'function') {
try {
method(...(args || []));
} catch (error:any) {
console.error(`Error calling method ${name} with args ${JSON.stringify(args || [])}: ${error.message}`);
}
} else {
console.warn(`Method ${name} does not exist in monitorModule`);
}
} else {
throw new Error('启动的方法描述符必须是字符串或者包含name属性的对象');
}
})
}
main/index.ts
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
// 监听方法启动器
import { onMonitorFunction } from './monitor/index'
// 文件监听模块
import * as FileModule from './monitor/fileMonitor'
// 窗口监听模块
import * as WindowModule from './monitor/windowMonitor'
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
frame: false,//设置无边框
center: true,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false,//采用这种方法必须设置为false
webSecurity:false
}
})
mainWindow.loadFile('http://loaclhost:3000');
// 加载监听器
onMonitorFunction(FileModule, ["getFilePath", "openFile"])
onMonitorFunction(WindowModule, ["closeWindow", { name: 'maxWindow', args: [mainWindow] }])
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
createWindow()
app.on('activate', function () {
// 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) createWindow()
})
})
// Quit when all windows are closed, except on 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()
}
})
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
六、代码仓库
gitee:gitee.com/liu_soon/el…
后记
文中代码有一部分是和后端交互的,主要用于对数据进行交互。但我并没有贴出后端的相关代码,主要看思路,后端只是简单的curd,你可以使用其他方式进行数据存储