使用 Svelte 和 Electron 构建桌面应用程序
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
前几天我看了Felix Riesberg的演讲,他讲到如何使用JavaScript和Electron构建桌面应用程序,这让我很感兴趣,想看看Svelte用起来有多简单。结果发现它真的超级简单,开箱即用!
如果你只想获得一个入门模板来创建你的应用程序,而不想阅读这篇博文,我在这里创建了一个模板。
本文将指导您使用 Svelte 和 Electron 创建一个基本的桌面 Markdown 编辑器。我们将能够编辑 Markdown 文件并查看并排预览,还可以使用 Electron API 和 NodeJS实现打开文件、创建新文件和保存文件等功能。
你可以在我的博客上看到成品的GIF动画:https://joshuaj.co.uk/building-desktop-app-svelte-electron
我们开始吧!
准备就绪🐣
-
克隆 Svelte 模板:
npx degit sveltejs/template svelte-markdown-editor -
进入新目录并运行
npm install以安装 Svelte 依赖项。 -
运行
npm install electron marked此命令将安装 Electron,然后安装标记的库,我们将使用该库将 Markdown 转换为 HTML。 -
在项目根目录下创建一个
main.js文件,这将是我们的 Electron 入口点。 -
在 `<script>` 标签内
package.json,将启动脚本从 `"sirv public"<script> ` 更改为"npm run build && electron main.js"`<script>`,这将构建 Svelte 应用程序,然后告诉 Electron 运行它。 -
编辑该
public/index.html文件,使所有路径.前面都加上 `\n`。例如:`\n`/build/bundle.css变为 `./build/bundle.css\n`。否则,Electron 将无法找到这些文件。 -
最后,将以下 meta 标签添加到 index.html 文件的 head 部分:
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />。对于这个小型应用程序来说,这不会造成任何问题,但如果没有添加,我们会在控制台中收到警告。点击此处
了解更多信息。
将 Svelte 与 Electron 连接起来🖇️
现在我们已经完成了样板设置,让我们返回到main.js之前创建的文件,将我们的 Svelte 应用与 Electron 连接起来。
一个基本的 Electron 应用由一个组件app和一个框架组成BrowserWindow,所以让我们导入它们:
const { app, BrowserWindow } = require("electron");
这app是我们用来处理应用程序生命周期的组件,而这BrowserWindow是实际渲染 Svelte 应用程序的窗口。
应用程序准备就绪后,首先创建一个新的浏览器窗口;关闭所有窗口后,退出应用程序:
app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
});
app.on("window-all-closed", () => {
app.quit();
});
请注意,我们将其设置nodeIntegration为 true,这样我们就可以在 Svelte 文件中使用 NodeJS 代码。
然后,在创建 BrowserWindow 的地方下方,告诉它打开 Svelte 构建时输出的 index.html 文件:
mainWindow.loadFile("./public/index.html");
现在,如果你运行它npm start,应该会打开一个窗口,显示基本的 Hello World Svelte 应用程序。
创建基础 Markdown 编辑器📝
在实现使用 Electron API 打开、保存和创建新文件的功能之前,让我们先移除 Svelte 样板代码并实现一个基本的 Markdown 编辑器。
在文件中src/main.js,删除 props 参数:
const app = new App({
target: document.body
});
接下来,创建一个名为:的组件Editor.svelte。
<script>
export let markdown = "";
</script>
<style>
section {
width: 100%;
min-height: 95vh;
border: 0.1rem solid #333;
flex: 1.5;
}
textarea {
width: 100%;
height: 100%;
min-height: 95vh;
resize: none;
margin: 0;
border: none;
}
</style>
<section>
<!-- svelte-ignore a11y-autofocus -->
<textarea bind:value={markdown} autofocus/>
</section>
该组件接收一个名为 `markdown` 的属性markdown,并将其绑定到一个文本区域,以便在文本区域更新时,`markdown` 变量也会自动更新。此外,由于我们的应用程序只有一个输入框,并且它是应用程序的主要焦点,因此我们还会自动聚焦文本区域并移除辅助功能警告。
然后创建一个名为Preview.svelte“
<script>
import marked from "marked";
export let markdown = "";
</script>
<style>
section {
width: 100%;
min-height: 95vh;
margin-left: 0.5rem;
padding: 0.2rem;
flex: 2;
background-color: #f5f5f5;
}
section :global(*) {
word-break: break-word;
white-space: pre-wrap;
}
</style>
<section>
{@html marked(markdown)}
</section>
该组件接收一段 Markdown 文本,然后使用marked库将其转换为 HTML。之后,我们使用 Svelte 标签直接将转换后的 HTML 内容输出到页面上{@html}。
样式方面并没有什么特别之处,你会注意到我们使用了 `<div>`来定位该部分下的所有内容。这是因为我们想要定位该部分下的所有 HTML,但 Svelte 认为不存在 HTML(因为它是动态生成的),所以如果我们直接使用 `<div>`:global(*)就会收到警告。css-unused-selector*
在App.svelte文件中将所有内容替换为:
<script>
import Editor from "./Editor.svelte";
import Preview from "./Preview.svelte";
let markdown = "# You can enter markdown here\n\n**This text is bold**\n\n_This text is italic_";
let activeFilePath;
</script>
<style>
main {
display: flex;
flex-direction: column;
}
.file-path {
font-weight: bold;
}
.editor-and-preview {
display: flex;
flex-direction: row;
}
</style>
<main>
<p class="file-path">
{activeFilePath ? activeFilePath : "Press 'Save' or hit 'CTRL + S' to save"}
</p>
<div class="editor-and-preview">
<Editor bind:markdown />
<Preview {markdown} />
</div>
</main>
这里我们存储一个变量,并告诉 Svelte 将其绑定到组件内部markdown的值,而我们知道该值绑定到文本区域的输入框。因此,我们输入的任何内容都会存储在这个变量中,然后我们将其传递给组件以渲染成 HTML。markdownEditormarkdownPreview
我们还存储了一个activeFilePath变量,稍后我们将使用该变量来显示编辑器中打开的文件。目前,由于没有文件路径,我们只会向用户显示一条消息,告诉他们如何保存。
现在运行程序后,npm start你应该会看到一个可用的 Markdown 编辑器,并带有实时更新的预览功能。
使用 Electron 保存文件💾
现在我们可以编写 Markdown 代码了,让我们给用户提供保存他们所写内容的功能。
在App.svelte文件中导入ipcRenderer ,这将允许我们监听来自Electron的消息以及向Electron发送消息:
const { ipcRenderer } = require("electron");
在同一个脚本标签内,我们使用该标签ipcRenderer监听"savefile"消息(消息名称可以是任何你想要的名称),当用户点击菜单项或使用 CTRL + S 时,我们最终会从 Electron 发送此消息。
ipcRenderer.on("savefile", () => {
});
在这个函数内部,我们首先要检查是否存在 activeFilePath,如果存在,则向 Electron 发送一条消息,告诉它将 markdown 变量中的内容保存到 activeFilePath 处的文件中;如果不存在,则发送一条消息,告诉 Electron 询问用户文件应该保存到哪里。
看起来如下:
ipcRenderer.on("savefile", () => {
if (activeFilePath) {
ipcRenderer.send("saveexistingfile", {
path: activeFilePath,
content: markdown
});
} else {
ipcRenderer.send("savenewfile", markdown);
}
});
目前这样做还不会有任何效果,因为我们还没有在主进程(我一直把主进程称为“Electron”)中实现相应的消息监听器和功能。接下来我们就来实现这些功能,首先从保存一个新文件开始。
在main.js文件中,从 electron 导入 Menu、MenuItem、ipcMain 和 dialog,并从 nodejs 标准库导入 fs:
const {
app,
BrowserWindow,
Menu,
MenuItem,
ipcMain,
dialog
} = require("electron");
const fs = require("fs");
createNewFile接下来,在回调函数内部创建一个名为 `save_file` 的函数app.whenReady().then,该函数接受一个字符串参数。该函数应该打开一个对话框,要求用户命名文件并选择保存位置,然后将文件保存到该位置,文件名包含提供的字符串。
*相关文档dialog请访问https://www.electronjs.org/docs/api/dialog
回调函数内部app.whenReady().then:
const createNewFile = content => {
dialog.showSaveDialog(mainWindow, {
title: "Create New File",
properties: ["showOverwriteConfirmation"],
filters: [
{
name: "Markdown Files",
extensions: ["md"]
}
]
})
.then(({ canceled, filePath }) => {
if (canceled) return;
fs.writeFile(filePath, content, err => {
if (err) return;
});
});
};
在上面的代码中,我们Electron.dialog打开一个保存对话框,该对话框只会保存(和显示)markdown 文件,然后,如果用户没有关闭对话框,我们就使用该fs模块将文件写入他们选择的文件路径。
现在我们有了函数,让我们编写代码,以便在收到"savenewfile"消息时调用它。
在同一位置,编写以下代码:
ipcMain.on("savenewfile", (e, content) => {
createNewFile(content);
});
在保存新文件之前,还有最后一步。如果您还记得的话,上面的消息监听器只有在 Svelte 应用发送“savenewfile”消息时才会触发,而“savenewfile”消息只能在 Svelte 应用收到“savefile”消息时发送。因此,当用户从菜单中选择保存或按下 CTRL + S 时,我们必须从 Electron 发送“savefile”消息。
您可以通过在回调函数中创建一个新的菜单app.whenReady().then,然后向其中添加 MenuItem 对象来实现这一点,如下所示:
const menu = new Menu();
menu.append(
new MenuItem({
label: "Save",
accelerator: "CmdOrCtrl+S",
click: () => mainWindow.webContents.send("savefile")
})
);
这里我们说的是,当按下 CTRL + S 或点击标签为“保存”的菜单项时,向 BrowserWindow(我们的 Svelte 应用程序)发送一条名为 的消息"savefile"。
最后一点,我们必须将菜单设置为应用程序菜单,如下所示:
Menu.setApplicationMenu(menu);
现在,运行程序npm start,输入一些 Markdown 代码,然后按下 CTRL + S(或菜单项)。你应该会看到一个保存对话框,输入文件名并点击保存后,文件应该就会保存到你选择的目录中。
下一步是添加保存现有文件时的消息监听器。在与上一步相同的位置,添加以下代码:
ipcMain.on("saveexistingfile", (e, { path, content }) => {
fs.writeFile(path, content, err => {
if (err) return;
});
});
这段代码只是使用该fs模块在现有路径创建一个新文件,并将新内容覆盖现有文件。
如果这让你感到困惑,这里有一张图表,展示了渲染进程(我们的 Svelte 应用)和主进程(Electron)之间的消息流转方式:
您也可以在此处阅读更多关于渲染器和主进程的信息。
接下来,我们将添加打开/保存现有文件或使用菜单创建新文件的功能。
打开/保存现有文件📂💾
这种方法原理基本相同,而且我觉得甚至更简单!
首先,在App.svelte文件中创建一个新的消息监听器,监听"fileopened"包含文件路径和文件内容的消息。收到此消息后,更新变量,以便 Svelte 更新我们的 HTML:
ipcRenderer.on("fileopened", (e, { path, content }) => {
activeFilePath = path;
markdown = content;
});
然后在main.js文件中,与该函数相同的位置createNewFile,创建一个openFile函数,该函数将向用户显示类似的对话框,但用于选择现有文件:
const openFile = () => {
const file = dialog.showOpenDialogSync(mainWindow, {
properties: ["openFile"],
filters: [{ name: "Markdown Files", extensions: ["md"] }]
});
if (file) {
fs.readFile(file[0], "utf8", (err, data) => {
if (err) return;
mainWindow.webContents.send("fileopened", {
path: file[0],
content: data
});
});
}
};
我们获取用户的选择,读取文件及其路径,然后"fileopened"向我们的 Svelte 应用发送消息。同时,我们也在函数"fileopened"中发送一条消息createNewFile,以便在保存新文件时更新 activeFilePath:
...})
.then(({ canceled, filePath }) => {
if (canceled) return;
fs.writeFile(filePath, content, err => {
if (err) return;
mainWindow.webContents.send("fileopened", {
path: filePath,
content
});
});
});
接下来,我们来添加一个用于打开文件的菜单项和键盘快捷键:
menu.append(
new MenuItem({
label: "Open",
accelerator: "CmdOrCtrl+O",
click: openFile
})
);
现在运行程序后npm start,你应该可以点击菜单中的“打开”按钮,或者按下 CTRL + O 键,弹出文件浏览器对话框。如果你选择一个现有的 Markdown 文件,它应该会在编辑器中打开。我们差不多完成了!
最后,添加一个新的菜单项,该菜单项将创建一个包含一些模板内容的新 Markdown 文件:
menu.append(
new MenuItem({
label: "Create New File",
accelerator: "CmdOrCtrl+N",
click: () =>
createNewFile(
"# You can enter markdown here\n\n**This text is bold**\n\n_This text is italic_"
)
})
);
🎉 你现在应该可以正常使用 Markdown 编辑器了!
为您的现有平台构建 🏗️
要打包应用程序,我们可以使用名为electron-packager的 CLI 工具。
-
首先,请在本地安装该软件包:
npm install electron-packager --save-dev -
然后
sirv-cli从 package.json 文件中删除该包(对于 electron 应用来说,我们不需要它)。 -
将
electron包裹移入devDependencies -
在 package.json 文件中添加一个指向入口文件的“main”属性:
"main": "./main.js" -
在 package.json 文件中添加“productName”属性:
"productName": "markdown-editor" -
在脚本中添加一个名为“package”的脚本:
"package": "npm install && npm run build && electron-packager ." -
运行
npm run package以构建适用于您当前平台的 Electron 应用
这将创建一个文件夹,其中包含您的可执行文件(在 Windows 系统上)以及相关文件(在 macOS 系统上)。您可以点击此处查看 electron-packager 文档了解更多信息。
结论
我们仅用少量代码就利用 Svelte 和 Electron 构建了一个非常基础的 Markdown 编辑器。这篇博文只是一个入门指南,如果您想更深入地开发,请参考 Electron 官方文档或更详细的教程。
一些补充说明
-
我们通过在渲染进程(Svelte)和主进程(Electron)之间发送消息来使用 Electron 和 Node API。另一种方法是直接在 Svelte 文件中使用 Electron 和 Node API。
-
这是我第一次尝试使用 Electron,请将本指南/教程视为 Electron 的入门介绍,而不是正确的使用方法。
PS:我将于三月份在伦敦Svelte聚会上就此主题做一个非常简短的闪电演讲。
文章来源:https://dev.to/pjaerr/building-a-desktop-app-using-svelte-and-electron-18ad

