Vue3和Electron實(shí)現(xiàn)桌面端應(yīng)用詳解
為了方便記錄一些個(gè)人隨筆,我最近用Laravel
和Vue 3.0
擼了一個(gè)博客系統(tǒng),其中使用到了一個(gè)基于markdown-it 的 markdown
編輯器Vue組件v-md-editor。我感覺用它去編寫markdown
還是很方便的。后面就有了一個(gè)想法,基于此組件用Electron來實(shí)現(xiàn)一個(gè)markdown
桌面端應(yīng)用,自己平時(shí)拿來使用也是個(gè)不錯(cuò)的選擇。
題外話:VS Code就是用Electron開發(fā)出來的桌面應(yīng)用,我現(xiàn)在除了移動(dòng)端的開發(fā)外,其他的都是使用VS Code來開發(fā)了,各種插件開發(fā)起來真的很方便。
接下來我就帶大家來一步步來實(shí)現(xiàn)這個(gè)功能。
Vue CLI 搭建Vue項(xiàng)目
在選擇的目錄下執(zhí)行vue create electron-vue3-mark-down
選擇自定義的模板(可以選擇默認(rèn)的Vue 3 模板)
選擇Vue3 和 TypeScript, 其他的選項(xiàng)基于自身項(xiàng)目決定是否選擇
執(zhí)行npm run serve
看看效果
Vue項(xiàng)目改造為markdown編輯器
執(zhí)行npm i @kangc/v-md-editor@next -S
安裝v-md-editor
添加TypeScript類型定義文件
由于v-md-editor這個(gè)庫(kù)沒有TypeScript類型定義文件,我就直接在shims-vue.d.ts這個(gè)文件的后面添加的,當(dāng)然也可以新建一個(gè)文件添加申明(tsconfig.json能找到這個(gè)文件就OK)。
declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } <!-- 添加的內(nèi)容 --> 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
<template> <div> <v-md-editor v-model="content" height="100vh"></v-md-editor> </div> </template> <script lang="ts"> // 編輯器 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"; // 快捷復(fù)制代碼 import createCopyCodePlugin from "@kangc/v-md-editor/lib/plugins/copy-code/index"; import "@kangc/v-md-editor/lib/plugins/copy-code/copy-code.css"; // 行號(hào) 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 }; }, }); </script> <style> /* 去掉一些按鈕 */ .v-md-icon-save, .v-md-icon-fullscreen { display: none; } </style>
這個(gè)文件也很簡(jiǎn)單,整個(gè)頁(yè)面就是一個(gè)編輯器
<v-md-editor v-model="content" height="100vh"></v-md-editor>
,這個(gè)markdown編輯器有高亮顯示,代碼顯示行號(hào),復(fù)制代碼按鈕等插件,當(dāng)然更方便的是可以添加其他的插件豐富這個(gè)markdown編輯器的功能.
效果如下
Vue CLI Plugin Electron Builder
我嘗試過用Vite 2.0去搭建Electron項(xiàng)目,但是沒有找到類似的Vite和Electron結(jié)合好使的工具,所以放棄了Vite 2.0的誘惑。如果有小伙伴有推薦可以分享下。
使用vue add electron-builder
安裝,我選擇的是13.0.0的Electron的最新版本。
我一般是選擇最高的版本,其實(shí)這個(gè)版本有坑,我后面再想想要不要介紹下這個(gè)坑,哈哈。
我們看到新加了很多的依賴庫(kù),還添加了一個(gè)
background.ts
文件。簡(jiǎn)單介紹下,這個(gè)文件執(zhí)行在主線程,其他的頁(yè)面都是在渲染線程。渲染線程有很多限制的,有些功能只能在主線程執(zhí)行,這里就不具體展開了。
執(zhí)行npm run electron:serve
看效果
至此,就可以看到桌面應(yīng)用的效果了,并且邊修改Vue的代碼,桌面應(yīng)用也能實(shí)時(shí)看到修改后的效果。
優(yōu)化功能
啟動(dòng)全屏顯示
引入screen
import { screen } from "electron";
創(chuàng)建窗口的時(shí)候設(shè)置為screen大小
<!-- background.ts --> async function createWindow() { const { width, height } = screen.getPrimaryDisplay().workAreaSize; const win = new BrowserWindow({ width, height, // 省略... }); // 省略... }
這樣應(yīng)用啟動(dòng)的時(shí)候就是全屏顯示了。
修改菜單欄
定義菜單欄
<!-- background.ts --> const template: Array<MenuItemConstructorOptions> = [ { label: "MarkDown", submenu: [ { label: "關(guān)于", 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: "存儲(chǔ)", accelerator: "CmdOrCtrl+S", click: ( item: MenuItem, focusedWindow: BrowserWindow | undefined, _event: KeyboardEvent ) => { // TODO: 存儲(chǔ)內(nèi)容 }, }, ], }, { label: "編輯", submenu: [ { label: "撤銷", accelerator: "CmdOrCtrl+Z", role: "undo", }, { label: "重做", accelerator: "Shift+CmdOrCtrl+Z", role: "redo", }, { type: "separator", }, { label: "剪切", accelerator: "CmdOrCtrl+X", role: "cut", }, { label: "復(fù)制", 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: "學(xué)習(xí)更多", click: function () { shell.openExternal("http://electron.atom.io"); }, }, ], }, ];
具體如何定義參閱Electron Menu。
打開文件和存儲(chǔ)目前還沒實(shí)現(xiàn),后面實(shí)現(xiàn)。
設(shè)置菜單欄
import { Menu } from "electron"; app.on("ready", async () => { // 省略... // 創(chuàng)建菜單 Menu.setApplicationMenu(Menu.buildFromTemplate(template)); });
在
ready
鉤子函數(shù)中進(jìn)行設(shè)置Menu。
效果
編輯器打開markdonw文件的內(nèi)容
主線程選擇文件,將文件路徑傳給渲染線程
<!-- background.ts --> 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);
這個(gè)方法將文件路徑傳給渲染線程。
渲染線程取到文件路徑,讀取文件內(nèi)容,賦值給markdown編輯器
<!-- App.vue --> 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支持
<!-- vue.config.js --> module.exports = { pluginOptions: { electronBuilder: { nodeIntegration: true, }, }, };
效果
markdonw的內(nèi)容存入文件
主線程發(fā)起向渲染線程獲取編輯器內(nèi)容的請(qǐng)求
<!-- background.js --> if (focusedWindow) { focusedWindow.webContents.send("get-content", ""); }
渲染線程主線程向返回編輯器的內(nèi)容
<!-- App.vue --> onMounted(() => { ipcRenderer.on("get-content", () => { ipcRenderer.send("save-content", content.value); }); });
主線程收到內(nèi)容然后存入文件
<!-- background.ts --> // 存儲(chǔ)文件 ipcMain.on("save-content", (event: unknown, content: string) => { if (openedFile.length > 0) { // 直接存儲(chǔ)到文件中去 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); }); } } });
效果
打包
設(shè)置應(yīng)用的名字和圖片
<!-- vue.config.js --> module.exports = { pluginOptions: { electronBuilder: { nodeIntegration: true, // 添加的設(shè)置 builderOptions: { appId: "com.johnny.markdown", productName: "JJMarkDown", // 應(yīng)用的名字 copyright: "Copyright © 2021", //版權(quán)聲明 mac: { icon: "./public/icon.icns", // icon }, }, }, }, };
icon.icns生成 準(zhǔn)備一個(gè)1024*1024的圖片,同級(jí)目錄下創(chuàng)建一個(gè)為icons.iconset
的文件夾;
創(chuàng)建各種不同尺寸要求的圖片文件
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
獲得名為icon.icns的圖標(biāo)文件
iconutil -c icns icons.iconset -o icon.icns
打包
npm run electron:build
結(jié)果
獲得的dmg文件就可以直接安裝使用了。
代碼
<!-- background.ts --> "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 = ""; // 存儲(chǔ)文件 ipcMain.on("save-content", (event: unknown, content: string) => { if (openedFile.length > 0) { // 直接存儲(chǔ)到文件中去 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<MenuItemConstructorOptions> = [ { label: "MarkDown", submenu: [ { label: "關(guān)于", 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: "存儲(chǔ)", 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: "復(fù)制", 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: "學(xué)習(xí)更多", 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(); // 創(chuàng)建菜單 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(); }); } }
到此這篇關(guān)于Vue3和Electron實(shí)現(xiàn)桌面端應(yīng)用詳解的文章就介紹到這了,更多相關(guān)Vue3 Electron 桌面端應(yīng)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
el-date-picker日期選擇限制范圍的實(shí)例代碼
這篇文章主要介紹了el-date-picker日期選擇限制范圍,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09Vue.Draggable實(shí)現(xiàn)拖拽效果
這篇文章主要介紹了Vue.Draggable實(shí)現(xiàn)拖拽效果,使用簡(jiǎn)單方便 ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06vue created鉤子函數(shù)與mounted鉤子函數(shù)的用法區(qū)別
這篇文章主要介紹了vue created鉤子函數(shù)與mounted鉤子函數(shù)的用法區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11vue3中如何通過ref和$parent結(jié)合defineExpose實(shí)現(xiàn)父子組件之間的通信
這篇文章主要介紹了vue3中通過ref和$parent結(jié)合defineExpose實(shí)現(xiàn)父子組件之間的通信,Vue3中通過ref和$parent的結(jié)合使用,及defineExpose的方法,可以非常便捷地實(shí)現(xiàn)父子組件之間的通信,需要的朋友可以參考下2023-07-07vue的v-if里實(shí)現(xiàn)調(diào)用函數(shù)
這篇文章主要介紹了vue的v-if里實(shí)現(xiàn)調(diào)用函數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07Vue項(xiàng)目打包部署到iis服務(wù)器的配置方法
這篇文章主要介紹了Vue項(xiàng)目打包部署到iis服務(wù)器的配置方法,文中通過代碼示例給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10vue+django實(shí)現(xiàn)一對(duì)一聊天功能的實(shí)例代碼
這篇文章主要介紹了vue+django實(shí)現(xiàn)一對(duì)一聊天功能,主要是通過websocket,由于Django不支持websocket,所以我使用了django-channels。,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2019-07-07