热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Vue3和Electron实现桌面端应用

为了方便记录一些个人随笔,我最近用Laravel和Vue3.0撸了一个博客系统,其中使用到了一个基于markdown-it的

为了方便记录一些个人随笔,我最近用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实现桌面端应用
    vue create
  • 选择Vue3TypeScript, 其他的选项基于自身项目决定是否选择

Vue3和Electron实现桌面端应用
vue3 + TypeScript
  • 执行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; 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

// 编辑器 import VMdEditor from "@kangc/v-md-editor"; import "@kangc/v-md-editor/lib/style/base-editor.css"; import vuepress from "@kangc/v-md-editor/lib/theme/vuepress.js"; import "@kangc/v-md-editor/lib/theme/style/vuepress.css"; // 高亮显示 import Prism from "prismjs"; import "prismjs/components/prism-json"; import "prismjs/components/prism-dart"; import "prismjs/components/prism-c"; import "prismjs/components/prism-swift"; import "prismjs/components/prism-kotlin"; import "prismjs/components/prism-java"; // 快捷 import createCopyCodePlugin from "@kangc/v-md-editor/lib/plugins/copy-code/index"; import "@kangc/v-md-editor/lib/plugins/copy-code/copy-code.css"; // 行号 import createLineNumbertPlugin from "@kangc/v-md-editor/lib/plugins/line-number/index"; VMdEditor.use(vuepress, { Prism, }) .use(createCopyCodePlugin()) .use(createLineNumbertPlugin()); import { defineComponent, ref } from "vue"; export default defineComponent({ name: "App", components: { VMdEditor }, setup() { const cOntent= ref(""); return { content }; }, }); /* 去掉一些按钮 */ .v-md-icon-save, .v-md-icon-fullscreen { display: none; }

这个文件也很简单,整个页面就是一个编辑器,这个markdown编辑器有高亮显示,代码显示行号,按钮等插件,当然更方便的是可以添加其他的插件丰富这个markdown编辑器的功能.

  • 效果如下
Vue3和Electron实现桌面端应用
编辑器效果

Vue CLI Plugin Electron Builder

我尝试过用Vite 2.0去搭建Electron项目,但是没有找到类似的ViteElectron结合好使的工具,所以放弃了Vite 2.0的诱惑。如果有小伙伴有推荐可以分享下。

  • 使用vue add electron-builder安装,我选择的是13.0.0Electron的最新版本。

我一般是选择最高的版本,其实这个版本有坑,我后面再想想要不要介绍下这个坑,哈哈。

Vue3和Electron实现桌面端应用
效果

我们看到新加了很多的依赖库,还添加了一个background.ts文件。简单介绍下,这个文件执行在主线程,其他的页面都是在渲染线程。渲染线程有很多限制的,有些功能只能在主线程执行,这里就不具体展开了。

  • 执行npm run electron:serve看效果
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"); }, }, ], }, ];

  1. 具体如何定义参阅Electron Menu。

  2. 打开文件存储目前还没实现,后面实现。

  • 设置菜单栏

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); });

  1. showOpenDialog是打开文件的方法,我们这里指定了只打开md文件;

  2. 获得文件路径后,通过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 = readFileSync(filePath).toString(); } }); }); return { content }; }, });

  • vue添加node支持

module.exports = { pluginOptions: { electronBuilder: { nodeIntegration: true, }, }, };

  • 效果
Vue3和Electron实现桌面端应用
效果图
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); }); } } });

  • 效果
Vue3和Electron实现桌面端应用
效果图

打包

  • 设置应用的名字和图片

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

  • icon.icns生成
  1. 准备一个1024*1024的图片,同级目录下创建一个为icons.iconset的文件夹;
  2. 创建各种不同尺寸要求的图片文件

sips -z 16 16 icon.png -o icons.iconset/icon_16x16.png sips -z 32 32 icon.png -o icons.iconset/icon_16x16@2x.png sips -z 32 32 icon.png -o icons.iconset/icon_32x32.png sips -z 64 64 icon.png -o icons.iconset/icon_32x32@2x.png sips -z 128 128 icon.png -o icons.iconset/icon_128x128.png sips -z 256 256 icon.png -o icons.iconset/icon_128x128@2x.png sips -z 256 256 icon.png -o icons.iconset/icon_256x256.png sips -z 512 512 icon.png -o icons.iconset/icon_256x256@2x.png sips -z 512 512 icon.png -o icons.iconset/icon_512x512.png sips -z 1024 1024 icon.png -o icons.iconset/icon_512x512@2x.png

  1. 获得名为icon.icns的图标文件

iconutil -c icns icons.iconset -o icon.icns

  • 打包

npm run electron:build

  • 结果
Vue3和Electron实现桌面端应用
dmg

获得的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 mode await 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 development win.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 + Q if (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 Devtools try { 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 === "graceful-exit") { app.quit(); } }); } else { process.on("SIGTERM", () => { app.quit(); }); } }

推荐阅读
  • Vue3中setup函数的参数props和context配置详解
    本文详细介绍了Vue3中setup函数的参数props和context的配置方法,包括props的接收和配置声明,以及未通过props进行接收配置时的输出值。同时还介绍了父组件传递给子组件的值和模板的相关内容。阅读本文后,读者将对Vue3中setup函数的参数props和context的配置有更深入的理解。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文介绍了如何按需加载elementui的部分模块,以及如何设置覆盖某些属性。通过import引入Dialog模块,并使用Vue.component进行全局设置。同时使用Vue.use引入ElementUI和VueAxios模块。通过extends进行属性覆盖设置。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 网络请求模块选择——axios框架的基本使用和封装
    本文介绍了选择网络请求模块axios的原因,以及axios框架的基本使用和封装方法。包括发送并发请求的演示,全局配置的设置,创建axios实例的方法,拦截器的使用,以及如何封装和请求响应劫持等内容。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文介绍了如何使用vue-awesome-swiper组件,包括在main.js中引入和使用swiper和swiperSlide组件,以及设置options和ref属性。同时还介绍了如何在模板中使用swiper和swiperSlide组件,并展示了如何通过循环渲染swipes数组中的数据,并使用picUrl属性显示图片。最后还介绍了如何添加分页器。 ... [详细]
  • uniapp开发H5解决跨域问题的两种代理方法
    本文介绍了uniapp开发H5解决跨域问题的两种代理方法,分别是在manifest.json文件和vue.config.js文件中设置代理。通过设置代理根域名和配置路径别名,可以实现H5页面的跨域访问。同时还介绍了如何开启内网穿透,让外网的人可以访问到本地调试的H5页面。 ... [详细]
  • 本文讨论了将HashRouter改为Router后,页面全部变为空白页且没有报错的问题。作者提到了在实际部署中需要在服务端进行配置以避免刷新404的问题,并分享了route/index.js中hash模式的配置。文章还提到了在vueJs项目中遇到过类似的问题。 ... [详细]
  • 微信民众号商城/小顺序商城开源项目介绍及使用教程
    本文介绍了一个基于WeiPHP5.0开发的微信民众号商城/小顺序商城的开源项目,包括前端和后端的目录结构,以及所使用的技术栈。同时提供了项目的运行和打包方法,并分享了一些调试和开发经验。最后还附上了在线预览和GitHub商城源码的链接,以及加入前端交流QQ群的方式。 ... [详细]
  • 一、路由首先需要配置路由,就是点击good组件进入goodDetail组件配置路由如下{path:goodDetail,component:goodDetail}同时在good组件中写入如下点击事件,路由中加入 ... [详细]
  • jmeter实践:从csv中获取带引号的数据详情的技巧和运行全部数据的方法
    本文分享了jmeter实践中从csv中获取带引号的数据的解决办法,包括设置CSV Data Set Config和运行脚本获取数据的方法。另外还介绍了循环运行csv中全部数据的解决方法,避免每次修改csv用例都需要修改脚本的麻烦。通过了解和掌握工具的细节点,可以更好地解决问题和提高技术水平。 ... [详细]
author-avatar
手机用户2502853923
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有