Vue3和Electron实现桌面端应用详解

目录

  • Vue CLI 搭建Vue项目
  • Vue项目改造为markdown编辑器
    • Vue CLI Plugin Electron Builder
  • 优化功能
    • 启动全屏显示
    • 修改菜单栏
    • 编辑器打开markdonw文件的内容
    • markdonw的内容存入文件
    • 打包
为了方便记录一些个人随笔,我最近用LaravelVue 3.0撸了一个博客系统,其中使用到了一个基于markdown-it 的 markdown 编辑器Vue组件v-md-editor。我感觉用它去编写markdown还是很方便的。后面就有了一个想法,基于此组件用Electron来实现一个markdown桌面端应用,自己平时拿来使用也是个不错的选择。
题外话:VS Code就是用Electron开发出来的桌面应用,我现在除了移动端的开发外,其他的都是使用VS Code来开发了,各种插件开发起来真的很方便。
接下来我就带大家来一步步来实现这个功能。

Vue CLI 搭建Vue项目 在选择的目录下执行vue create electron-vue3-mark-down
选择自定义的模板(可以选择默认的Vue 3 模板)

Vue3和Electron实现桌面端应用详解
文章图片

选择Vue3 和 TypeScript, 其他的选项基于自身项目决定是否选择
Vue3和Electron实现桌面端应用详解
文章图片

执行npm run serve看看效果
Vue3和Electron实现桌面端应用详解
文章图片


Vue项目改造为markdown编辑器 执行npm i @kangc/v-md-editor@next -S安装v-md-editor
添加TypeScript类型定义文件
由于v-md-editor这个库没有TypeScript类型定义文件,我就直接在shims-vue.d.ts这个文件的后面添加的,当然也可以新建一个文件添加申明(tsconfig.json能找到这个文件就OK)。
declare module "*.vue" {import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; }declare module "@kangc/v-md-editor/lib/theme/vuepress.js"; declare module "@kangc/v-md-editor/lib/plugins/copy-code/index"; declare module "@kangc/v-md-editor/lib/plugins/line-number/index"; declare module "@kangc/v-md-editor"; declare module "prismjs";

改造App.vue
/* 去掉一些按钮 */.v-md-icon-save,.v-md-icon-fullscreen {display: none; }

这个文件也很简单,整个页面就是一个编辑器,这个markdown编辑器有高亮显示,代码显示行号,复制代码按钮等插件,当然更方便的是可以添加其他的插件丰富这个markdown编辑器的功能.
效果如下
Vue3和Electron实现桌面端应用详解
文章图片


Vue CLI Plugin Electron Builder
我尝试过用Vite 2.0去搭建Electron项目,但是没有找到类似的Vite和Electron结合好使的工具,所以放弃了Vite 2.0的诱惑。如果有小伙伴有推荐可以分享下。
使用vue add electron-builder安装,我选择的是13.0.0的Electron的最新版本。
我一般是选择最高的版本,其实这个版本有坑,我后面再想想要不要介绍下这个坑,哈哈。
Vue3和Electron实现桌面端应用详解
文章图片

我们看到新加了很多的依赖库,还添加了一个background.ts文件。简单介绍下,这个文件执行在主线程,其他的页面都是在渲染线程。渲染线程有很多限制的,有些功能只能在主线程执行,这里就不具体展开了。
执行npm run electron:serve看效果
Vue3和Electron实现桌面端应用详解
文章图片

【Vue3和Electron实现桌面端应用详解】至此,就可以看到桌面应用的效果了,并且边修改Vue的代码,桌面应用也能实时看到修改后的效果。

优化功能
启动全屏显示
引入screen
import { screen } from "electron";

创建窗口的时候设置为screen大小
async function createWindow() {const { width, height } = screen.getPrimaryDisplay().workAreaSize; const win = new BrowserWindow({width,height,// 省略...}); // 省略...}

这样应用启动的时候就是全屏显示了。

修改菜单栏
定义菜单栏
const template: Array = [{label: "MarkDown",submenu: [{label: "关于",accelerator: "CmdOrCtrl+W",role: "about",},{label: "退出程序",accelerator: "CmdOrCtrl+Q",role: "quit",},],},{label: "文件",submenu: [{label: "打开文件",accelerator: "CmdOrCtrl+O",click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,_event: KeyboardEvent) => {// TODO: 打开文件},},{label: "存储",accelerator: "CmdOrCtrl+S",click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,_event: KeyboardEvent) => {// TODO: 存储内容},},],},{label: "编辑",submenu: [{label: "撤销",accelerator: "CmdOrCtrl+Z",role: "undo",},{label: "重做",accelerator: "Shift+CmdOrCtrl+Z",role: "redo",},{type: "separator",},{label: "剪切",accelerator: "CmdOrCtrl+X",role: "cut",},{label: "复制",accelerator: "CmdOrCtrl+C",role: "copy",},{label: "粘贴",accelerator: "CmdOrCtrl+V",role: "paste",},],},{label: "窗口",role: "window",submenu: [{label: "最小化",accelerator: "CmdOrCtrl+M",role: "minimize",},{label: "最大化",accelerator: "CmdOrCtrl+M",click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,_event: KeyboardEvent) => {if (focusedWindow) {focusedWindow.maximize(); }},},{type: "separator",},{label: "切换全屏",accelerator: (function () {if (process.platform === "darwin") {return "Ctrl+Command+F"; } else {return "F11"; }})(),click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,// eslint-disable-next-line @typescript-eslint/no-unused-vars_event: KeyboardEvent) => {if (focusedWindow) {focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); }},},],},{label: "帮助",role: "help",submenu: [{label: "学习更多",click: function () {shell.openExternal("http://electron.atom.io"); },},],},];

具体如何定义参阅Electron Menu。
打开文件和存储目前还没实现,后面实现。
设置菜单栏
import { Menu } from "electron"; app.on("ready", async () => {// 省略...// 创建菜单Menu.setApplicationMenu(Menu.buildFromTemplate(template)); });

ready钩子函数中进行设置Menu。
效果
Vue3和Electron实现桌面端应用详解
文章图片


编辑器打开markdonw文件的内容
主线程选择文件,将文件路径传给渲染线程
dialog.showOpenDialog({properties: ["openFile"],filters: [{ name: "Custom File Type", extensions: ["md"] }],}).then((res) => {if (res && res["filePaths"].length > 0) {const filePath = res["filePaths"][0]; // 将文件传给渲染线程if (focusedWindow) {focusedWindow.webContents.send("open-file-path", filePath); }}}).catch((err) => {console.log(err); });

showOpenDialog是打开文件的方法,我们这里指定了只打开md文件;
获得文件路径后,通过focusedWindow.webContents.send("open-file-path", filePath); 这个方法将文件路径传给渲染线程。
渲染线程取到文件路径,读取文件内容,赋值给markdown编辑器
import { ipcRenderer } from "electron"; import { readFileSync } from "fs"; export default defineComponent({// 省略...setup() {const content = ref(""); onMounted(() => {// 1.ipcRenderer.on("open-file-path", (e, filePath: string) => {if (filePath && filePath.length > 0) {// 2.content.value = https://www.it610.com/article/readFileSync(filePath).toString(); }}); }); return { content }; },});

vue添加node支持
module.exports = {pluginOptions: {electronBuilder: {nodeIntegration: true,},},};

效果


markdonw的内容存入文件
主线程发起向渲染线程获取编辑器内容的请求
if (focusedWindow) {focusedWindow.webContents.send("get-content", ""); }

渲染线程主线程向返回编辑器的内容
onMounted(() => {ipcRenderer.on("get-content", () => {ipcRenderer.send("save-content", content.value); }); });

主线程收到内容然后存入文件
// 存储文件ipcMain.on("save-content", (event: unknown, content: string) => {if (openedFile.length > 0) {// 直接存储到文件中去try {writeFileSync(openedFile, content); console.log("保存成功"); } catch (error) {console.log("保存失败"); }} else {const options = {title: "保存文件",defaultPath: "new.md",filters: [{ name: "Custom File Type", extensions: ["md"] }],}; const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) {dialog.showSaveDialog(focusedWindow, options).then((result: Electron.SaveDialogReturnValue) => {if (result.filePath) {try {writeFileSync(result.filePath, content); console.log("保存成功"); openedFile = result.filePath; } catch (error) {console.log("保存失败"); }}}).catch((error) => {console.log(error); }); }}});

效果


打包
设置应用的名字和图片
module.exports = {pluginOptions: {electronBuilder: {nodeIntegration: true,// 添加的设置builderOptions: {appId: "com.johnny.markdown", productName: "JJMarkDown",// 应用的名字copyright: "Copyright ? 2021", //版权声明mac: {icon: "./public/icon.icns", // icon},},},},};

icon.icns生成 准备一个1024*1024的图片,同级目录下创建一个为icons.iconset的文件夹;
创建各种不同尺寸要求的图片文件
sips -z 16 16 icon.png -o icons.iconset/icon_16x16.pngsips -z 32 32 icon.png -o icons.iconset/icon_16x16@2x.pngsips -z 32 32 icon.png -o icons.iconset/icon_32x32.pngsips -z 64 64 icon.png -o icons.iconset/icon_32x32@2x.pngsips -z 128 128 icon.png -o icons.iconset/icon_128x128.pngsips -z 256 256 icon.png -o icons.iconset/icon_128x128@2x.pngsips -z 256 256 icon.png -o icons.iconset/icon_256x256.pngsips -z 512 512 icon.png -o icons.iconset/icon_256x256@2x.pngsips -z 512 512 icon.png -o icons.iconset/icon_512x512.pngsips -z 1024 1024 icon.png -o icons.iconset/icon_512x512@2x.png

获得名为icon.icns的图标文件
iconutil -c icns icons.iconset -o icon.icns

打包
npm run electron:build

结果
Vue3和Electron实现桌面端应用详解
文章图片

获得的dmg文件就可以直接安装使用了。
代码
"use strict"; import {app,protocol,BrowserWindow,screen,Menu,MenuItem,shell,dialog,ipcMain,} from "electron"; import { KeyboardEvent, MenuItemConstructorOptions } from "electron/main"; import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; import installExtension, { VUEJS3_DEVTOOLS } from "electron-devtools-installer"; const isDevelopment = process.env.NODE_ENV !== "production"; import { writeFileSync } from "fs"; let openedFile = ""; // 存储文件ipcMain.on("save-content", (event: unknown, content: string) => {if (openedFile.length > 0) {// 直接存储到文件中去try {writeFileSync(openedFile, content); console.log("保存成功"); } catch (error) {console.log("保存失败"); }} else {const options = {title: "保存文件",defaultPath: "new.md",filters: [{ name: "Custom File Type", extensions: ["md"] }],}; const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) {dialog.showSaveDialog(focusedWindow, options).then((result: Electron.SaveDialogReturnValue) => {if (result.filePath) {try {writeFileSync(result.filePath, content); console.log("保存成功"); openedFile = result.filePath; } catch (error) {console.log("保存失败"); }}}).catch((error) => {console.log(error); }); }}}); const template: Array = [{label: "MarkDown",submenu: [{label: "关于",accelerator: "CmdOrCtrl+W",role: "about",},{label: "退出程序",accelerator: "CmdOrCtrl+Q",role: "quit",},],},{label: "文件",submenu: [{label: "打开文件",accelerator: "CmdOrCtrl+O",click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,// eslint-disable-next-line @typescript-eslint/no-unused-vars_event: KeyboardEvent) => {dialog.showOpenDialog({properties: ["openFile"],filters: [{ name: "Custom File Type", extensions: ["md"] }],}).then((res) => {if (res && res["filePaths"].length > 0) {const filePath = res["filePaths"][0]; // 将文件传给渲染线程if (focusedWindow) {focusedWindow.webContents.send("open-file-path", filePath); openedFile = filePath; }}}).catch((err) => {console.log(err); }); },},{label: "存储",accelerator: "CmdOrCtrl+S",click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,// eslint-disable-next-line @typescript-eslint/no-unused-vars_event: KeyboardEvent) => {if (focusedWindow) {focusedWindow.webContents.send("get-content", ""); }},},],},{label: "编辑",submenu: [{label: "撤销",accelerator: "CmdOrCtrl+Z",role: "undo",},{label: "重做",accelerator: "Shift+CmdOrCtrl+Z",role: "redo",},{type: "separator",},{label: "剪切",accelerator: "CmdOrCtrl+X",role: "cut",},{label: "复制",accelerator: "CmdOrCtrl+C",role: "copy",},{label: "粘贴",accelerator: "CmdOrCtrl+V",role: "paste",},],},{label: "窗口",role: "window",submenu: [{label: "最小化",accelerator: "CmdOrCtrl+M",role: "minimize",},{label: "最大化",accelerator: "CmdOrCtrl+M",click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,// eslint-disable-next-line @typescript-eslint/no-unused-vars_event: KeyboardEvent) => {if (focusedWindow) {focusedWindow.maximize(); }},},{type: "separator",},{label: "切换全屏",accelerator: (function () {if (process.platform === "darwin") {return "Ctrl+Command+F"; } else {return "F11"; }})(),click: (item: MenuItem,focusedWindow: BrowserWindow | undefined,// eslint-disable-next-line @typescript-eslint/no-unused-vars_event: KeyboardEvent) => {if (focusedWindow) {focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); }},},],},{label: "帮助",role: "help",submenu: [{label: "学习更多",click: function () {shell.openExternal("http://electron.atom.io"); },},],},]; protocol.registerSchemesAsPrivileged([{ scheme: "app", privileges: { secure: true, standard: true } },]); async function createWindow() {const { width, height } = screen.getPrimaryDisplay().workAreaSize; const win = new BrowserWindow({width,height,webPreferences: {nodeIntegration: true,contextIsolation: false,},}); if (process.env.WEBPACK_DEV_SERVER_URL) {// Load the url of the dev server if in development modeawait win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string); if (!process.env.IS_TEST) win.webContents.openDevTools(); } else {createProtocol("app"); // Load the index.html when not in developmentwin.loadURL("app://./index.html"); }}// Quit when all windows are closed.app.on("window-all-closed", () => {// On macOS it is common for applications and their menu bar// to stay active until the user quits explicitly with Cmd + Qif (process.platform !== "darwin") {app.quit(); }}); 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) createWindow(); }); // 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.on("ready", async () => {if (isDevelopment && !process.env.IS_TEST) {// Install Vue Devtoolstry {await installExtension(VUEJS3_DEVTOOLS); } catch (e) {console.error("Vue Devtools failed to install:", e.toString()); }}createWindow(); // 创建菜单Menu.setApplicationMenu(Menu.buildFromTemplate(template)); }); // Exit cleanly on request from parent process in development mode.if (isDevelopment) {if (process.platform === "win32") {process.on("message", (data) => {if (data =https://www.it610.com/article/=="graceful-exit") {app.quit(); }}); } else {process.on("SIGTERM", () => {app.quit(); }); }}

到此这篇关于Vue3和Electron实现桌面端应用详解的文章就介绍到这了,更多相关Vue3 Electron 桌面端应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    推荐阅读