使用 Electron 和 Vue 构建桌面应用程序
已发表: 2022-03-10JavaScript 曾经被称为构建网站和 Web 应用程序的语言,尤其是它的一些框架,如 React、Vue 和 Angular,但随着时间的推移(早在 2009 年),JavaScript 可以在浏览器之外运行Node.js 的出现,这是一个开源、跨平台的 JavaScript 运行时环境,可在 Web 浏览器之外执行 JavaScript 代码。 这使得 JavaScript 不仅可以用于 Web 应用程序,还可以用于更多方面,其中之一就是使用 Electron.js 构建桌面应用程序。
Electron 通过提供具有丰富原生(操作系统)API 的运行时,使您能够使用纯 JavaScript 创建桌面应用程序。 您可以将其视为 Node.js 运行时的变体,专注于桌面应用程序而不是 Web 服务器。
在本教程中,我们将学习如何使用 Electron 构建桌面应用程序,我们还将学习如何使用 Vuejs 构建 Electron 应用程序。
注意:学习本教程需要 Vue.js 和 Vue CLI 的基本知识。 本教程中使用的所有代码都可以在我的 GitHub 上找到。 随意克隆和玩弄它!
什么是桌面应用程序?
桌面应用程序是在台式机或膝上型计算机中独立运行的应用程序。 它们是执行特定任务的应用程序,仅为此目的而安装。
桌面应用程序的一个示例是您的 Microsoft Word,它用于创建和键入文档。 常见桌面应用程序的其他示例包括 Web 浏览器、Visual Studio Code 和 Adobe Photoshop。 桌面应用程序与 Web 应用程序不同,因为您必须安装桌面应用程序才能访问和使用它,而且它们有时不需要互联网访问即可工作。 另一方面,可以通过简单地访问托管此类应用程序的 URL 来访问 Web 应用程序,并且始终需要 Internet 访问权限才能访问它们。
用于构建桌面应用程序的框架示例包括:
- 爪哇
Java 是一种通用的编程语言,它是基于类的、面向对象的,并且设计为具有尽可能少的实现依赖关系。 它旨在让应用程序开发人员编写一次,随处运行(WORA),这意味着编译后的 Java 代码可以在所有支持 Java 的平台上运行,而无需重新编译。 - 爪哇外汇
根据他们的官方文档,它是一个开源的下一代客户端应用程序平台,适用于基于 Java 的桌面、移动和嵌入式系统。 - C#
C# 是一种通用的多范式编程语言,包括强类型、词法范围、命令式、声明式、函数式、通用、面向对象和面向组件的编程学科。 - 。网
.NET 是一个免费的、跨平台的、开源的开发者平台,用于构建许多不同类型的应用程序。 借助 .NET,您可以使用多种语言、编辑器和库来构建 Web、移动、桌面、游戏和 IoT。
什么是电子?
Electron 是一个用于构建桌面应用程序的开源框架。 它以前称为“Atom shell”,由 GitHub 开发和维护。 它允许您使用 HTML、CSS 和 JavaScript 编写跨平台桌面应用程序。 这意味着您可以使用一个代码库为 Windows、MacOS 和其他平台构建桌面应用程序。 它基于 Node.js 和 Chromium。 使用 Electron 构建的应用程序示例包括流行的 Atom 编辑器、Visual Studio Code、桌面版 Wordpress 和 Slack。
安装
你可以使用 NPM 在你的项目中安装 Electron:
npm install electron --save-dev
如果您要使用以下命令大量使用电子应用程序,您也可以全局安装它:
npm install electron -g
使用 Electron 为桌面构建 Vuejs 应用程序
如果您熟悉使用 Vuejs 构建 Web 应用程序,则可以使用 Vuejs 构建桌面应用程序。 为此,您只需要 Vue CLI Plugin Electron Builder。
Vue CLI 插件 Electron Builder
该工具允许您使用 Electron 为桌面构建 Vue 应用程序,这意味着它可以使您的 Vue 应用程序作为电子应用程序工作。 这意味着您的 Vue 应用程序(可能是 Web 应用程序)可以扩展为在桌面环境中工作,而无需在另一个框架中构建单独的桌面应用程序。 这为 Vue 开发人员提供了超越 Web 的选项和能力。 展望未来,您可以实现您的想法,并为用户提供桌面应用程序选项 - 可以在 Windows、macOS 和 Linux 上运行的选项。
为了看到这一点,我们将使用 News API 构建一个 News 应用程序。 该应用程序将提供突发新闻标题,并允许您使用其 API 从网络上的新闻来源和博客中搜索文章。 开始使用它们所需的只是您的个人 API 密钥,可以从这里获取。
我们将构建一个简单的应用程序,它提供以下功能:
- 显示来自所选国家/地区的热门和重要标题的页面,可以选择使用其
/top-headlines
端点选择国家/地区。 新闻 API 提供来自他们支持的国家/地区列表的新闻,请在此处找到列表。 - 来自选定类别的新闻,使用它们的
/everything
端点和查询参数q
的组合,我们将使用它指定我们的类别。
获得您的 API 密钥后,我们可以使用 Vue CLI 创建我们的应用程序。 确保您的系统上安装了 Vue CLI,如果没有,请使用以下命令安装它:
npm install -g @vue/cli # OR yarn global add @vue/cli
完成后,使用 CLI 创建您的新闻应用程序:
vue create news-app
在本教程中,我们将使用 Axios 从 News API 获取数据,但您可以使用任何您更熟悉的替代方案。 您可以使用以下任何命令安装 Axios:
//NPM npm install axios // YARN yarn add axios
下一步是在我们的应用程序中为全局配置设置一个 Axios 实例。 我们将在src文件夹中创建一个plugins文件夹,我们将在其中创建这个axios.js文件。 创建文件后,添加以下代码行:
import axios from "axios"; let baseURL = `https://newsapi.org/v2`; let apiKey = process.env.VUE_APP_APIKEY; const instance = axios.create({ baseURL: baseURL, timeout: 30000, headers: { "X-Api-Key": apiKey, }, }); export default instance;
在这里,我们定义了从 News API 获得的baseURL
和apiKey
,并将其传递给 Axios 的新实例。 此实例接受baseURL
和apiKey
以及timeout
属性。 News API 要求您在向他们的 API 发出请求时添加您的 API 密钥,并提供 3 种将其附加到您的请求的方法,但在这里,我们将其添加到标头X-Api-Key
属性中,然后导出instance
。 完成后,我们现在可以将此配置用于我们应用程序中的所有 Axios 请求。
完成后,您可以使用以下命令在 CLI 中添加 Plugin Electron 构建器:
vue add electron-builder
系统将提示您选择首选的 Electron 版本,我选择了9.0.0
版本,因为它是 Electron 的最新版本(在撰写本文时)。
完成后,您现在可以使用以下命令为您的应用程序提供服务:
Using Yarn(strongly recommended) yarn electron:serve OR NPM npm run electron:serve
这将需要一些时间来编译和提供您的应用程序。 完成后,您的应用程序将在您的系统上弹出,如下所示:
如果您关闭应用程序的开发工具,它应该如下所示:
这个电子插件非常有用且易于使用,因为该应用程序开发的每个部分都与 Vue 应用程序的工作方式相同。 这意味着您的 Web 应用程序和桌面应用程序都可以拥有一个代码库。 我们的应用程序将包含三个部分:
- 一个着陆页,呈现随机选择的国家/地区的头条新闻。
- 用于呈现来自用户选择的国家/地区的热门新闻的页面。
- 呈现来自用户选择的类别的热门新闻的页面。
为此,我们将需要一个用于所有导航链接的标题组件。 因此,让我们在components文件夹中创建一个文件并将其命名为header.vue ,然后在其中添加以下代码行:
<template> <header class="header"> <div class="logo"> <div class="logo__container"> <img src="../assets/logo.png" alt="News app logo" class="logo__image" /> </div> <h1>News App</h1> </div> <nav class="nav"> <h4 class="nav__link"> <router-link to="/home">Home</router-link> </h4> <h4 class="nav__link"> <router-link to="/top-news">Top News</router-link> </h4> <h4 class="nav__link"> <router-link to="/categories">News By Category</router-link> </h4> </nav> </header> </template> <script> export default { name: "app-header", }; </script> <style> .header { display: flex; flex-wrap: wrap; justify-content: space-between; } .logo { display: flex; flex-wrap: nowrap; justify-content: space-between; align-items: center; height: 50px; } .logo__container { width: 50px; height: 50px; } .logo__image { max-width: 100%; max-height: 100%; } .nav { display: flex; flex-wrap: wrap; width: 350px; justify-content: space-between; } </style>
在这里,我们创建了一个标头组件,其中包含我们的应用程序名称和徽标(图片可以在我的 GitHub 上找到)以及一个包含指向我们应用程序其他部分的链接的导航部分。 接下来是在我们的布局页面上导入这个页面——App.vue ,这样我们就可以在每个页面上看到我们的标题。
<template> <div> <app-header /> <router-view /> </div> </template> <script> import appHeader from "@/components/Header.vue"; export default { name: "layout", components: { appHeader, }, }; </script> <style> @import url("https://fonts.googleapis.com/css2?family=Abel&family=Staatliches&display=swap"); html, #app { min-height: 100vh; } #app { font-family: "Abel", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; background-color: #fff; } #app h1 { font-family: "Staatliches", cursive; } a { font-weight: bold; color: #2c3e50; text-decoration: none; } a:hover { text-decoration: underline; } a.router-link-exact-active { color: #42b983; } </style>
在这里,我们将模板部分中的默认内容替换为我们在脚本部分中导入并声明后新创建的标题组件。 最后,我们在样式部分为整个应用添加一些样式。
现在,如果我们尝试查看我们的应用程序,它应该如下所示:
下一步是将内容添加到我们的Home.vue文件中。 该页面将托管我们应用程序的第一部分; 来自随机选择的国家/地区的热门新闻。 使用以下代码行更新您的Home.vue文件:
<template> <section class="home"> <h1>Welcome to News App</h1> <h4>Displaying Top News from {{ countryInfo.name }}</h4> <div class="articles__div" v-if="articles"> <news-card v-for="(article, index) in articles" :key="index" :article="article" ></news-card> </div> </section> </template> <script> import { mapActions, mapState } from "vuex"; import NewsCard from "../components/NewsCard"; export default { data() { return { articles: "", countryInfo: "", }; }, components: { NewsCard, }, mounted() { this.fetchTopNews(); }, computed: { ...mapState(["countries"]), }, methods: { ...mapActions(["getTopNews"]), async fetchTopNews() { let countriesLength = this.countries.length; let countryIndex = Math.floor( Math.random() * (countriesLength - 1) + 1 ); this.countryInfo = this.countries[countryIndex]; let { data } = await this.getTopNews( this.countries[countryIndex].value ); this.articles = data.articles; }, }, }; </script> <style> .articles__div { display: flex; flex-wrap: wrap; justify-content: center; } </style>
在这个文件的脚本部分,我们从 Vuex 导入mapState
和mapActions
,我们稍后将在这个文件中使用它们。 我们还导入了一个NewsCard
组件(我们将在接下来创建它),它将在此页面上呈现所有新闻标题。 然后,我们使用fetchTopNews
方法从我们商店的countries
地区数组中随机选择一个国家/地区获取最新消息。 该国家/地区被传递给我们的getTopNews
操作,这将被附加到baseURL
作为对国家/地区的查询,例如baseURL/top-news?country=${randomCountry}
。 完成后,我们循环遍历这些数据并将其传递给模板部分中Newscard
组件的article
属性。 我们还有一段表明头条新闻来自哪个国家。
接下来是设置我们的NewsCard
组件来显示这个新闻。 在您的组件文件夹中创建一个新文件,将其命名为NewsCard.vue ,并向其中添加以下代码行:
<template> <section class="news"> <div class="news__section"> <h1 class="news__title"> <a class="article__link" :href="article.url" target="_blank"> {{ article.title }} </a> </h1> <h3 class="news__author" v-if="article.author">{{ article.author }}</h3> <!-- <p class="article__paragraph">{{ article.description }}</p> --> <h5 class="article__published">{{ new Date(article.publishedAt) }}</h5> </div> <div class="image__container"> <img class="news__img" src="../assets/logo.png" :data-src="article.urlToImage" :alt="article.title" /> </div> </section> </template> <script> export default { name: "news-card", props: { article: Object, }, mounted() { this.lazyLoadImages(); }, methods: { lazyLoadImages() { const images = document.querySelectorAll(".news__img"); const options = { // If the image gets within 50px in the Y axis, start the download. root: null, // Page as root rootMargin: "0px", threshold: 0.1, }; const fetchImage = (url) => { return new Promise((resolve, reject) => { const image = new Image(); image.src = url; image.onload = resolve; image.onerror = reject; }); }; const loadImage = (image) => { const src = image.dataset.src; fetchImage(src).then(() => { image.src = src; }); }; const handleIntersection = (entries) => { entries.forEach((entry) => { if (entry.intersectionRatio > 0) { loadImage(entry.target); } }); }; // The observer for the images on the page const observer = new IntersectionObserver(handleIntersection, options); images.forEach((img) => { observer.observe(img); }); }, }, }; </script> <style> .news { width: 100%; display: flex; flex-direction: row; align-items: flex-start; max-width: 550px; box-shadow: 2px 1px 7px 1px #eee; padding: 20px 5px; box-sizing: border-box; margin: 15px 5px; border-radius: 4px; } .news__section { width: 100%; max-width: 350px; margin-right: 5px; } .news__title { font-size: 15px; text-align: left; margin-top: 0; } .news__author { font-size: 14px; text-align: left; font-weight: normal; } .article__published { text-align: left; } .image__container { width: 100%; max-width: 180px; max-height: 180px; } .news__img { transition: max-width 300ms cubic-bezier(0.4, 0, 1, 1), max-height 300ms cubic-bezier(0.4, 0, 1, 1); max-width: 150px; max-height: 150px; } .news__img:hover { max-width: 180px; max-height: 180px; } .article__link { text-decoration: none; color: inherit; } </style>
在这里,我们使用article
对象道具显示传递到该组件的数据。 我们还有一种方法可以延迟加载附加到每篇文章的图像。 此方法循环遍历我们拥有的文章图像的数量,并在它们变得可见时延迟加载它们。 最后,我们在样式部分中有针对此组件的样式。
下一件事将是建立我们的商店,以便我们可以开始获取最新消息。 将以下代码行添加到您的index.js文件中:
import Vue from "vue"; import Vuex from "vuex"; import axios from "../plugins/axios"; Vue.use(Vuex); const store = new Vuex.Store({ state: { countries: [{ name: "United States of America", value: "us", }, { name: "Nigeria", value: "ng", }, { name: "Argentina", value: "ar", }, { name: "Canada", value: "ca", }, { name: "South Africa", value: "za", }, ], categories: [ "entertainment", "general", "health", "science", "business", "sports", "technology", ], }, mutations: {}, actions: { async getTopNews(context, country) { let res = await axios({ url: `/top-headlines?country=${country}`, method: "GET", }); return res; }, }, }); export default store;
我们正在向我们的商店添加两个属性,其中一个属性是countries
。 该属性包含一个国家对象数组。 我们还有categories
属性; 这包含 News API 上可用类别的数组。 读者会喜欢自由查看来自特定国家和类别的热门新闻; 这在应用程序的多个部分中也需要,这就是我们使用商店的原因。 在我们商店的操作部分,我们有一个getTopNews
方法,它从一个国家/地区获取头条新闻(该国家/地区是从调用此操作的组件传递的)。
此时,如果我们打开我们的应用程序,我们应该会看到我们的登录页面,如下所示:
background.js 文件
此文件是 Electron 进入您的应用程序的入口点。 它控制此应用程序的所有类似桌面应用程序的设置。 这个文件的默认状态可以在我的 GitHub 上找到。
在这个文件中,我们为应用程序设置了一些预定义的配置,例如您的应用程序的默认height
和width
。 让我们看一下您可以在此文件中执行的一些操作。
启用 Vuejs 开发工具
默认情况下,您可以访问 Electron 中的开发工具,但安装后未启用。 这是由于 Windows 10 上存在的错误造成的,因此如果您打开background.js文件,您会发现一些注释掉的代码,其中的注释说明了它们被注释掉的原因:
// Install Vue Devtools // Devtools extensions are broken in Electron 6.0.0 and greater // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode // If you are not using Windows 10 dark mode, you may uncomment these lines // In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines // try { // await installVueDevtools() // } catch (e) { // console.error('Vue Devtools failed to install:', e.toString()) // }
因此,如果您不受此错误的影响,您可以取消注释try/catch
块并在同一文件(第 5 行)中搜索installVueDevtools
并取消注释。 完成此操作后,您的应用程序将自动重新启动,当您检查开发工具时,您应该会看到 Vuejs Devtools。
为您的应用选择自定义图标
默认情况下,Electron 图标被设置为您的应用程序的默认图标,并且大多数时候,您可能希望设置自己的自定义图标。 为此,请将您的图标移动到您的公用文件夹中,并将其重命名为icon.png 。 接下来要做的是添加所需的依赖项electron-icon-builder
。
您可以使用以下任何命令安装它:
// With Yarn: yarn add --dev electron-icon-builder // or with NPM: npm install --save-dev electron-icon-builder
完成后,您可以运行下一个命令。 它会将您的图标转换为 Electron 格式,并在完成后在您的控制台中打印以下内容。
接下来是在background.js文件中设置图标选项。 此选项位于从Electron
导入的BrowserWindow
选项中。 为此, BrowserWindow
更新为如下所示:
// Add this to the top of your file /* global __static */ // import path import path from 'path' // Replace win = new BrowserWindow({ width: 800, height: 600 }) // With win = new BrowserWindow({ width: 800, height: 600, icon: path.join(__static, 'icon.png') })
现在,如果我们运行yarn run electron:build
并查看我们的应用程序,我们应该会看到更新后的图标被用作应用程序图标,但它在开发过程中并没有改变。 此问题有助于解决在 macOS 上对其进行手动修复的问题。
为您的应用设置标题
您会注意到您的应用程序的标题设置为应用程序名称(在本例中为新闻应用程序),我们需要对其进行更改。 为此,我们必须在background.js
文件中的BrowserWindow
方法中添加一个title
属性,如下所示:
win = new BrowserWindow({ width: 600, height: 500, title: "News App", icon: path.join(__static, "icon.png"), webPreferences: { // Use pluginOptions.nodeIntegration, leave this alone // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, }, });
在这里,我们将应用程序的标题设置为“新闻应用程序”。 但是,如果您的index.html文件选择了标题或者您的标题没有更改为此,请尝试将此代码添加到您的文件中:
win.on("page-title-updated", (event) => event.preventDefault());
当我们的title
从BrowserWindow
更新时,我们正在监听一个被触发的事件。 当这个事件被触发时,我们告诉 Electron 不要用index.html文件中的标题更新标题。
可能值得更改的另一件事是productName
,它控制当您将鼠标悬停在应用程序图标上时出现的名称或您的计算机将应用程序识别为什么名称。 现在,我们的应用程序的名称是Electron
。 要在生产环境中更改此名称,请创建一个vue.config.js文件并向其中添加以下代码行:
module.exports = { pluginOptions: { electronBuilder: { builderOptions: { productName: "News App", }, }, }, };
在这里,我们将productName
定义为“News App”,这样当我们为我们的应用程序运行构建命令时,名称会从“Electron”更改为“News App”。
多平台构建
默认情况下,当您运行构建命令时,创建的应用程序取决于运行它的平台。 这意味着如果您在 Linux 上运行 build 命令,则创建的应用程序将是 Linux 桌面应用程序。 这同样适用于其他平台(macOS 和 windows)。 但是 Electron 提供了一个选项来指定你想要生成的平台(或两个平台)。 可用的选项有:
-
mac
-
win
-
linux
因此,为了构建应用程序的 Windows 版本,请运行以下命令:
// NPM npm electron:build -- --win nsis // YARN yarn electron:build --win nsis
结论
完成的应用程序可以在我的 GitHub 上找到。 官方 Electron 文档提供了信息和指南,可帮助您以任何方式自定义桌面应用程序。 我尝试过但未包含在本教程中的一些内容是:
- 在 macOS 上自定义您的扩展坞 — https://www.electronjs.org/docs/tutorial/macos-dock。
- 设置可调整大小、可最大化等等 - https://github.com/electron/electron/blob/master/docs/api/browser-window.md#new-browserwindowoptions。
因此,如果您希望使用 Electron 应用程序做更多事情,他们的官方文档是一个很好的起点。
相关资源
- Node.js https://en.wikipedia.org/wiki/Node.js
- Java(编程语言)https://en.wikipedia.org/wiki/Java_(programming_language)
- 电子(软件框架)
- JavaFX 14
- 电子
- 电子文档
- Vue CLI 插件 Electron Builder
- 使用 Chris Nwamba 的 Intersection Observer 延迟加载图像以提高性能
- axios
- 在 Nuxt 中开始使用 Axios https://www.smashingmagazine.com/2020/05/getting-started-axios-nuxt/) by Timi Omoyeni