【译】Creating a Markdown editor/previewer in Electron and Vue.js

使用Electron编写桌面应用程序是迄今为止我在职业生涯中使用过的最简单,最令人愉快的GUI应用程序工具包。 Electron是Atom,Visual Studio Code,Slack桌面客户端,Postman,GitKraken和Etcher等流行应用程序中使用的工具包。 每个都是功能强大,高性能,跨平台的应用程序,具有丰富的用户体验,为用户提供卓越的功能,每个平台上具有相同的用户界面界面,同时正确集成到平台中。

跨平台GUI工具包经常被嘲笑为速度慢,不能提供与平台的良好集成,或者在GUI功能方面有所妥协。 Electron通过基于谷歌Chrome浏览器的巧妙架构实现这一壮举。 也就是说,Electron包装Chromium的核心,以便您编写Node.js应用程序来管理包含应用程序窗口的一个或多个BrowserWindow对象。 顾名思义, BrowserWindow提供了基于最新版Chrome的完整HTML5,CSS3,JavaScript ES-2016编程环境。

换句话说 - 使用Electron,您可以使用最先进的Web技术来开发桌面GUI应用程序。

一个有趣的因素是,因为Electron基于Chromium,Chrome开发人员工具是内置的。这意味着Electron应用程序开发人员可以轻松获得优秀的Web应用程序调试和检查工具,这些工具已经过前端工程师的全面测试。世界。

Electron的架构有两套流程。 Main进程是前面提到的Node.js应用程序,用于管理应用程序的其余部分。 Renderer进程是前面提到的BrowserWindow实例。 每个都是一个顶级窗口,其中包含通过HTML + CSS + JavaScript代码指定的UI。 这些进程通过进程间通信通道相互通信。

虽然Electron使用Chromium,但安全模型与常规浏览器JavaScript有很大不同。 在Electron BrowserWindow中执行的代码可以访问Node.js模块,并可以访问文件系统。 有一些安全警告: https : //electronjs.org/docs/tutorial/security阅读安全警告非常重要。

要探索Electron,我们将使用您选择的HTML模板构建一个Markdown编辑器,其中包含生成的HTML的实时预览。 UI将使用Vue.js,Bulma / Buefy和ACE编辑器组件的组合构建。

结果可能是这样的应用程序:

目标

左侧是带有Markdown测试文件的ACE编辑器组件,右侧是呈现给HTML的Markdown的预览。 这是该应用程序的核心。 当然,完成的应用程序将需要工具栏,菜单选项和其他一些东西。 我们将完成一个应用程序,以提供一个有用的示例。

GitHub上提供了本文附带的源代码,以帮助阅读: https //github.com/sourcerer-io/electron-vue-buefy-editor

因为Electron基于Node.js和JavaScript,所以您需要熟悉它们。 可以使用Node.js创建一整套东西。 它是一个代码开发平台,用于在浏览器外部运行JavaScript,主要是在服务器环境中。 像Node.js Web Development这样的书可以让你开始使用Node.js. Electron将Node.js带入桌面应用程序开发领域。

构建编辑器脚手架

随着所有这些部件的组装,存在许多可能使应用程序开发变得困难的复杂性。 感谢各自的团队提供有用的应用程序入门代码,这项任务远没有那么困难。

Electron框架已经将一些复杂性捆绑到一个易于使用的包中。 例如,Electron文档( https://electronjs.org/docs/tutorial/first-app )包含用于启动简单演示应用程序的配方。 遵循这些说明并研究最终的应用程序非常有用。 但是,我们需要其他东西作为起点。

由于目标是使用Vue.js构建此应用程序,因此我们需要一个适用于该工具包的起点。 Vue.js应用程序通常是为网站构建的,所以我们需要一些不同的东西。

在这种情况下,我们想在Electron中运行Vue.js代码。 由于Electron UI是使用Web技术创建的,因此它当然可以支持Vue.js. Electron-Vue框架提供了我们所需的一切,一个支持在电子应用程序中使用Vue.js的构建和打包系统。https://simulatedgreg.gitbooks.io/electron-vue/

首先,安装Vue命令行工具:

<span style="color:rgba(0, 0, 0, 0.84)">  $ sudo npm install -g vue-cli </span>

vue-cli提供的主要功能是下载应用程序模板并设置启动器应用程序。 虽然它有一些内置模板,但它可用于下载第三方模板,包括Electron-Vue。

<span style="color:rgba(0, 0, 0, 0.84)">  $ vue init simulatedgreg / electron-vue electron-vue-buefy-editor </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ? 应用程序名称electron-vue-buefy-editor 
  ? 项目描述电子项目 
  ? 选择要安装vue-electron的Vue插件 
  ? 使用lint和ESLint? 没有 
  ? 使用Karma + Mocha设置单元测试? 没有 
  ? 使用Spectron + Mocha设置端到端测试? 没有 
  ? 你想用什么构建工具? 建设者 
  ? 作者David Herron < <a data-cke-saved-href="mailto:david@davidherron.com" href="mailto:david@davidherron.com" class="markup--anchor markup--pre-anchor">david@davidherron.com</a> > vue-cli·生成“electron-vue-buefy-editor”。 </span>
<span style="color:rgba(0, 0, 0, 0.84)">  -   -  </span>
<span style="color:rgba(0, 0, 0, 0.84)"> 可以了,好了。 欢迎来到您的新电子项目! </span>
<span style="color:rgba(0, 0, 0, 0.84)"> 请务必查看此样板文档 
  https://simulatedgreg.gitbooks.io/electron-vue/content/。 </span>
<span style="color:rgba(0, 0, 0, 0.84)"> 下一步: </span>
<span style="color:rgba(0, 0, 0, 0.84)">  $ cd electron-vue-buefy-editor 
  $ yarn(或`npm install`) 
  $ yarn run dev(或`npm run dev`) </span>
<span style="color:rgba(0, 0, 0, 0.84)"> 初始项目设置有许多替代方案。 在这个例子中,我们只安装了vue-electron,没有安装ESLint或单元测试支持,并使用电子构建器指定打包应用程序。 您可能更喜欢不同的设置,因此请根据需要回答问题。 </span>

您可以按照这些说明查看基本的Electron-Vue应用程序。

演示Electron-Vue应用程序

选择用于Vue.js的UI工具包组件

Vue.js是用于实现在Web浏览器中运行的组件的框架。 Vue.js应用程序是使用这些组件构建的,理论上可以使用Vue.js以及自定义开发的CSS和JavaScript完全构建UI。 但是,有一些开源项目提供预先烘焙的UI组件,内置响应式Web最佳实践。

由于Bootstrap非常受欢迎,因此首先想到的是使用Bootstrap。 虽然将Bootstrap集成到Vue.js应用程序很容易,但很快就会遇到问题。 Vue.js和jQuery非常不兼容,强烈建议不要在Vue.js中使用Bootstrap或jQuery。 问题是Vue.js期望严格控制DOM操作,因此无法使用jQuery等其他技术进行DOM操作。 有多个项目试图将Bootstrap代码集成为Vue.js组件,但没有一个项目支持Bootstrap 4。

一个广泛推荐的替代方案是Bulma工具包。 不能说这个名字有吸引力,但网站https://bulma.io/为Bulma提供了一个很好的案例,作为一个有价值的UI框架。 有超过100,000名开发人员使用Bulma,它是一个仅限CSS的工具包,使其轻量级,易于与Vue.js集成。 Buefy组件库将Bulma与Vue.js集成: https://buefy.github.io/#/我们将在应用程序中使用Buefy。

Buefy可以使用https://materialdesignicons.com/上设置的Material Design图标。这些图标可作为Node.js模块在https://www.npmjs.com/package/vue-material-design-icons上使用

要做的最后一个UI选择是用于编辑Markdown的组件。 虽然我们可以让用户在常规TextArea组件中编写Markdown,但我们可以做得更好。 例如,Ace编辑器组件为各种编码语言(如JavaScript或C ++或HTML)提供了非常称职的编辑体验。 我们可能希望编辑器能够支持编辑HTML模板,CSS文件或许多其他资源,而Ace编辑器组件可以处理所有这些。 Ace在其网站上有记录: https ://ace.c9.io/ Vue2 Ace Editor组件包用于在Vue.js中使用Ace: https: //github.com/chairuosen/vue2-ace-editor

将Buefy和ACE添加到Electron / Vue.js应用程序

我们已经初始化了一个空白的Vue.js应用程序,并选择了要使用的UI框架。让我们首先将这些组件与空白应用程序集成。

目录结构包括:

目录src/main包含Main进程的代码,而src/renderer包含Renderer进程的代码。 您将看到后者是Vue组件的存储位置。

首先是安装包依赖项:

<span style="color:rgba(0, 0, 0, 0.84)">  $ npm install buefy vue2-ace-editor vue-material-design-icons --save </span>

这将安装前面描述的Vue.js组件,以及它们的依赖项,例如Bulma框架和Ace编辑器组件。

编辑index.ejs ,这是Electron-Vue提供的布局模板,以匹配此代码:

<span style="color:rgba(0, 0, 0, 0.84)">  < <strong>html</strong> style =“height:100%;”> 
  < <strong>头</strong> > 
  < <strong>meta</strong> charset =“utf-8”> 
  < <strong>meta</strong> name =“viewport”content =“width = device-width,initial-scale = 1”> 
  < <strong>title</strong> > electron-vue-buefy-editor </ <strong>title</strong> > 
  .. 
  </ <strong>head</strong> > 
  < <strong>body</strong> style =“height:100%;”> .. </ <strong>body</strong> > 
  </ <strong>html</strong> > </span>

需要修改<html><body>标记,以便应用程序填充窗口的整个高度。

接下来我们将Buefy带入应用程序,将src/renderer/main.js的前面部分更改为:

<span style="color:rgba(0, 0, 0, 0.84)"> 从'vue'导入Vue; 
 从'./App'导入应用程序; 
 从'./router'导入路由器; 
 从'buefy'导入Buefy; 
  import'buefy / lib / buefy.css'; 
 从'util'导入util; </span>
<span style="color:rgba(0, 0, 0, 0.84)"> 从'电子'导入{ipcRenderer}; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  Vue.use(Buefy); </span>

我们将继续对此文件进行更多更改。 这部分将Buefy组件添加到Vue.js.

以这种方式添加Buefy遵循电子应用的最佳实践。 根据Electron文档( https://electronjs.org/docs/tutorial/security )中的Security页面,非常重要的是,不要从第三方Web服务加载代码。 阅读安全页面了解更多详情。 真。这不是一个闲置的建议,去看吧。

ipcRenderer对象用于从Main进程到Renderer进程的通信。

此时您可以执行npm run dev - 在开发模式下运行应用程序 - 并且看到没有任何更改。 然而,我们为有趣的事情奠定了基础。

实现编辑器应用程序

接下来,删除src/renderer/components下的每个文件。 这些文件包含演示屏幕,我们在应用程序中不需要它。

App.vue更改为:

<span style="color:rgba(0, 0, 0, 0.84)">  <模板> 
  <div id =“app”> 
  <编辑页> </编辑器页> 
  </ DIV> 
  </模板> </span>
<span style="color:rgba(0, 0, 0, 0.84)">  <SCRIPT> 
 从'@ / components / EditorPage'导入EditorPage 
  export default { 
 名称:'electron-vue-buefy-editor', 
 组件: { 
  EditorPage 
  } 
  } 
  </ SCRIPT> </span>
<span style="color:rgba(0, 0, 0, 0.84)">  <风格> 
  / * CSS * / 
  #app { 
 身高:100%; 
  } 
  </样式> </span>

第一个更改是使用名为EditorPage的组件作为应用程序的主要部分。 继续下一段,我们将创建该组件。 第二个更改是确保编辑器填充窗口的垂直高度。

Vue.js将其称为单个文件模板 。 还有另一种实现Vue组件的方法,它是可以引用其他文件(包括外部模板或CSS文件)的JavaScript代码。 正如其名称所示,单个文件模板将它们组合在一个文件中。

src/renderer/components创建EditorPage.vue ,它将定义EditorPage组件,并以<template><style>部分开头:

<span style="color:rgba(0, 0, 0, 0.84)">  < <strong>template</strong> > 
  < <strong>div</strong> id =“wrapper”> 
  < <strong>div</strong> id =“editor”class =“columns is-gapless is-mobile”> 
  < <strong>编辑</strong> 
  ID =” aceeditor” 
  REF =” aceeditor” 
 类=”塔” 
  V型=”输入” 
  @初始化=” editorInit” 
 郎=”降价” 
 主题=”暮光之城” 
 宽度=” 500px的” 
  height =“100%”> </ <strong>editor</strong> > 
  < <strong>preview-iframe</strong> 
  ID =” previewor” 
 类=”塔” 
  ref =“previewor”> </ <strong>preview-iframe</strong> > 
  </ <strong>div</strong> > 
  </ <strong>div</strong> > 
  </ <strong>template</strong> > </span>
<span style="color:rgba(0, 0, 0, 0.84)">  < <strong>style</strong> scoped> 
  #wrapper { 
 身高:100%; 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">  #editor { 
  / *保证金:4px;  * / 
 身高:100%; 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">  #previewor { 
  margin-left:2px; 
 身高:100%; 
  } 
  </ <strong>style</strong> > </span>

这将设置一个双列用户界面,一个包含editor组件,另一个包含preview-iframe 。 这两个组件实现了前面显示的用户界面,每个组件都是Vue.js调用自定义组件的。 当应用程序运行时,Vue.js会将每个内容插入到实际的HTML中。

此处显示的<template>中的所有内容都将位于App.vue<App/>标记内,因此将显示为应用程序。 在<style>部分,我们再次指定height100% ,以确保组件填充整个可用的垂直空间。

scoped标记在此组件中排列CSS以引用此组件生成的HTML。

vue2-ace-editor模块为我们提供了<editor>标签。 我们通过向EditorPage.vue添加<script>部分来实现这一点。

<span style="color:rgba(0, 0, 0, 0.84)">  <SCRIPT> 
 从'./PreviewIframe.vue'导入PreviewIframe; 
 从'../main.js'导入{messageBus}; 
 从'fs-extra'导入fs; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  export default { 
  data:function(){ 
 返回{ 
 输入:'#hello', 
  isNewFile:是的, 
  isChangedFile:false, 
 文件名: ””, 
  layoutFileName:“” 
  }; 
  }, 
 组件: { 
 编辑:require('vue2-ace-editor'), 
  previewIframe:PreviewIframe 
  }, 
 看:{ 
  input:function(newContent,oldContent){ 
  messageBus.newContentToRender(newContent); 
  this.isChangedFile = true; 
  } 
  }, 
 计算:{ 
  editor(){return this。$ refs.aceeditor;  }, 
  previewor(){return this。$ refs.previewor;  } 
  }, 
 方法: { 
  editorInit(编辑){ 
 要求( '支架/ EXT / language_tools'); 
 要求( '支架/模式/ HTML'); 
 要求( '支架/模式/降价'); 
 要求( '支架/主题/暮'); 
  editor.setWrapBehavioursEnabled(真); 
  editor.setShowInvisibles(真); 
  editor.setShowFoldWidgets(真); 
  editor.setShowPrintMargin(真); 
  。editor.getSession()setUseWrapMode(真); 
  editor.getSession()setUseSoftTabs(真)。 
  messageBus.newContentToRender(this.input); 
  }, 
  } 
  } 
  </ SCRIPT> </span>

在Vue.js中,使用像这样的匿名对象实例化Vue实例。 在像这样的单个文件模板中,使用默认导出描述Vue实例对象,如此处所示。

components字段列出了此组件使用的组件。 对象中每个条目的标记将成为模板中的标记名称。 因此,“ editor ”变为“ <editor> ”标签,而“ previewIframe ”变为模板中的“ <preview-iframe> ”标签。 稍后我们将定义PreviewIframe组件,所以请紧紧抓住。

data字段列出了实现此组件时使用的数据。 管理数据并通知侦听器对托管数据的更改是Vue.js的核心功能之一。 此处显示的结构不是托管的实际数据,而是Vue.js在构造Vue实例时的输入。 在幕后,Vue.js设置了观察者方法和通知方法,因此如果更改了托管数据项,则会发送通知事件并更新用户界面。 有关详细信息,请参阅: https : //vuejs.org/v2/guide/components.html

input字段包含Markdown文本。 isNewFile标志指示是否使用New菜单栏选项创建了内容 - 我们稍后将进入菜单栏 - 而isChangedFile指示是否已进行更改。 fileName字段记录与此内容关联的文件名(如果有)。layoutFileName选项记录要使用的布局模板。

editor组件上的v-model属性可确保将正在编辑的内容隐藏到data列出的input项中。

messageBus很快就会定义,它是我们用来在组件之间发送消息的机制。Vue.js有一个组件向其父节点发送消息的机制。 如果应用程序需要将消息发送到不是父组件的组件,该怎么办? 我们将为此目的使用messageBus 。

computed字段是用于导出值的函数的对象。 然后,组件中的其他代码将能够引用this.computedValue来检索派生值。 有关详细信息,请参阅此处: https : //vuejs.org/v2/guide/computed.html

在这种情况下,应用程序需要引用这两个组件。 在每一个中我们添加了一个ref=” identifier ”属性来帮助识别组件。 this.$refs对象填充了基于每个ref=” identifier ”的值的组件ref=” identifier ” 。 因此,这两个计算函数为我们提供了访问子组件的便捷简写。

editor组件上的@init属性导致vue2-ace-editor在初始化时发出消息。 我们在这里用它来初始化Ace编辑器的外观设置。 lang , theme , widthheight属性用于相同的目的。 一个非常重要的事情,因为我们将编写Markdown文本,就是设置自动换行。

watch字段允许我们定义在数据变量更改时调用的处理函数。 在这种情况下,应用程序必须知道内容何时更改,以便可以记录此事实,以便应用程序可以重新呈现预览的HTML。

EditorPage组件中将实现更多功能。 但是现在,让我们通过连接预览窗格来快速获胜。

PreviewIFrame组件

要预览与Markdown文本对应的HTML,我们将使用<iframe>和内置HTTP服务器。 原因是我们希望支持将Markdown渲染到任何布局模板中。 在<iframe>显示呈现的HTML将提供最准确的HTML表示。

在我们第一次尝试预览HTML时,我们只是将HTML插入到<div>组件中。但是该演示文稿受到了应用程序的CSS的影响,并且存在许多奇怪的内容,例如没有项目符号的列表。 通过使用<iframe>我们有一个空白的板,CSS智能,正确显示渲染的HTML。

创建一个名为src/renderer/components/PreviewIframe.vue其中包含:

<span style="color:rgba(0, 0, 0, 0.84)">  < <strong>template</strong> > 
  < <strong>iframe</strong> src =“”/> 
  </ <strong>template</strong> > </span>
<span style="color:rgba(0, 0, 0, 0.84)">  < <strong>script</strong> > 
 从'../main.js'导入{messageBus}; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  export default { 
 方法: { 
  reload(previewSrcURL){this。$ el.src = previewSrcURL;  } 
  }, 
  created:function(){ 
  messageBus。$ on('newContentToPreview',(url2preview)=> { 
  this.reload(url2preview); 
  }); 
  } 
  } 
  </ <strong>script</strong> > </span>
<span style="color:rgba(0, 0, 0, 0.84)">  < <strong>style</strong> scoped> 
  iframe {身高:100%;  } 
  </ <strong>style</strong> > </span>

该模板只是一个<iframe>标记。 在created函数中,我们在messageBusnewContentToPreview事件上设置一个监听messageBus ,然后调用reload ,它将iframesrc属性设置为提供的URL。

这种方法与典型的Vue.js组件实践不同。 通常会将previewSrcURL添加到props数组中,以将其作为可由其他代码设置的属性进行管理。

在这个应用程序中,正常的做法是行不通的。 我们的用户将在编辑器中input ,并且在每次击键时input的值都将被更改。 我们希望这些更改传播到呈现函数,然后传播到iframe将重新加载的此组件。

通过为src属性分配URL,可以重新加载iframe 。 因此,我们希望为每次重新渲染Markdown时可靠地更新src属性。 由于如果值没有变化,Vue.js不会触发对属性值的任何更新通知,我们改为使用此reload函数。

messageBus

我们已经看过messageBus已经多次提到了。 它是在应用程序中的组件之间发送数据的关键。

messageBus只是一个Vue实例。 Vue实例已经提供了事件订阅和分发机制,以及方法和数据管理等其他属性。 我们可以使用它作为跨组件数据交换的手段。

返回src/renderer/main.js并添加此代码

<span style="color:rgba(0, 0, 0, 0.84)">  export const messageBus = new Vue({ 
 方法: { 
  newContentToRender(newContent){ 
  ipcRenderer.send('newContentToRender',newContent); 
  }, 
  } 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ipcRenderer.on('newContentToPreview',(event,url2preview)=> { 
  messageBus。$ emit('newContentToPreview',url2preview); 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ipcRenderer.on('newFile2Edit',(event)=> { 
  。messageBus $发射( 'newFile2Edit'); 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ipcRenderer.on('editorDoUndo',(event)=> { 
  。messageBus $发射( 'editorDoUndo'); 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ipcRenderer.on('editorDoRedo',(event)=> { 
  messageBus $发射( 'editorDoRedo'); 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ipcRenderer.on('editorSelectAll',(event)=> { 
  。messageBus $发射( 'editorSelectAll'); 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ipcRenderer.on('openNewFile',(event,file2open)=> { 
  messageBus。$ emit('openNewFile',file2open); 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  ipcRenderer.on('saveCurrentFile',(event)=> { 
  。messageBus $发射( 'saveCurrentFile'); 
  }); </span>

这里还有比我们需要的更多的事件,我们将继续使用这些事件。 像这样调用new Vue会创建一个新的Vue实例。 messageBus Vue实例从此模块导出,您将注意到它已导入到EditorPagePreviewIframe 。

我们目前看到的是newContentToRender因为它是由EditorPage发送的。 这会调用ipcRenderer.send ,它会将消息发送到Main进程。

我们看到了许多ipcRenderer.on实例。 这是我们从Main进程接收消息的地方。 在每种情况下,使用messageBus.$emit发送相应的事件在messageBus上发送消息。

newContentToRender Vue实例方法用于向Main进程发送相应的消息。 当Renderer进程(顾名思义)具有必须呈现的新内容时,它将被发送。

在呈现内容时,从Main进程接收另一条消息newContentToPreview 。PreviewIframe组件侦听此事件并更新<iframe>以查看给定的URL。

因此, Main进程必须包含用于呈现Markdown到达newContentToRender消息的代码,使呈现的HTML在HTTP URL上可用,并使用相应的URL向Renderer进程发送newContentToPreview消息。 正如他们所说,非常容易完成。

预览服务器

如前所述,我们需要一个简单的HTTP服务器来提供呈现的HTML。

你可能会挠头,想知道发生了什么。 我们有一个<iframe>通过HTTP连接请求呈现的HTML到同一进程中的HTTP服务器。 我们去过疯了吗?

要求是使用HTTP向<iframe>提供HTML。 因此,我们需要一个HTTP服务器,它也可能在Main进程中运行。 在同一进程内管理HTTP服务器要比通过旋转和管理包含HTTP服务器的子进程容易得多。 Node.js可以轻松构建这种简单的HTTP服务器。

考虑到这一点,创建一个名为src/main/preview-server.js其中包含:

<span style="color:rgba(0, 0, 0, 0.84)"> 从'http'导入http; 
 从'url'导入网址; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  var服务器; 
  var内容; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  export function createServer(){ 
  if(server)抛出新错误(“服务器已启动”); 
  server = http.createServer(requestHandler); 
  server.listen(0,“127.0.0.1”); 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">  export function newContent(text){ 
  content = text; 
  return genurl('content'); 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">  export function currentContent(){ 
 返回内容; 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">  function genurl(pathname){ 
  const url2preview = url.format({ 
 协议:'http', 
  hostname:server.address()。address, 
  port:server.address()。port, 
  pathname:pathname 
  }); 
  return url2preview; 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">  function requestHandler(req,res){ 
 尝试{ 
  res.writeHead(200,{ 
  'Content-Type':'text / html', 
  'Content-Length':content.length 
  }); 
  res.end(内容); 
  } catch(err){ 
  res.writeHead(500,{ 
  'Content-Type':'text / plain' 
  }); 
  res.end(err.stack); 
  } 
  } </span>

通过调用createServer来初始化模块, createServer设置HTTPServer对象。我们没有提供端口号,而是为我们分配了一个端口号,因此我们不必担心端口号与计算机上的任何现有端口冲突。 因此, genurl函数查询server对象以找出IP地址和端口号。 另一个细节是我们强制它只监听IP地址127.0.0.1,以尽量减少不法分子潜入应用程序的机会。 可以采取另一步骤并生成添加到URL的随机令牌,并拒绝缺少有效令牌的连接请求。

每当呈现的内容可用时,我们将使用newContent函数通知我们。 内容存储在全局变量中。 此函数返回可用于获取内容的URL。 请注意,URL是硬编码的。

currentContent函数执行名称建议,并返回当前呈现的内容。 我们将使用它来实现“ 导出到HTML”功能。

使用什么URL并不重要,因为requestHandler函数对任何请求的URL进行相同的响应,即呈现的内容。

将Markdown呈现为HTML

现在我们有了预览服务器,让我们来看看如何将Markdown呈现为HTML。

Node.js有许多Markdown渲染器库。 在这个项目中,我们将使用Markdown-it,因为它非常受欢迎,它支持CommonMark。 它还支持一长串插件,增加额外的功能,如脚注。 开箱即用它支持类似Github的表。 请参阅https://www.npmjs.com/package/markdown-it

创建一个名为src/main/renderer.js的文件,其中包含:

<span style="color:rgba(0, 0, 0, 0.84)"> 从'markdown-it'导入mdit; 
 从'ejs'导入ejs; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  const mditConfig = { 
  html:true,xhtmlOut:true, 
  break:false,linkify:true, 
 印刷师:假的, 

  highlight:function(/ * str ,, lang * /){return'';  } 
  }; 
  const md = mdit(mditConfig); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  const layouts = []; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  export function renderContent(content,layoutFile){ 
  const text = md.render(content); 
  const layout = layouts [layoutFile]; 
  const rendered = ejs.render(layout,{ 
 标题:'页面标题', 
 内容:文字 
  }); 
 返回呈现; 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)"> 布局['layout1.html'] =` 
  <HTML> 
  <HEAD> 
  <title> <%= title%> </ title> 
  <风格> 
  H1,h2,h3,h4 {颜色:红色;  } 
 前{ 
 背景:rgb(59,58,58); 
 白颜色; 
  } 
  </样式> 
  </ HEAD> 
  <body> <% -  content%> </ body> 
  </ HTML>`; </span>

Markdown-它需要一个具有许多选项的配置对象。 这就是我们在模块顶部所做的事情。

renderContent函数不仅注意将Markdown呈现为HTML,而且还使用模板呈现该HTML。 该概念可能支持许多模板,这些模板将作为文件存储在文件系统中。 现在我们可以使用这个,并将其保存在内存中。

主要过程

现在我们可以通过修改Main进程来设置消息接收,并调度到渲染器和HTTP服务器模块来实现这一切。 在src/main/main.js开始进行更改

<span style="color:rgba(0, 0, 0, 0.84)">  import { 
  app,BrowserWindow,ipcMain,对话框 
 来自'电子'; 
 从'./renderer.js'导入{renderContent}; 
 从'./preview-server.js'导入{createServer,newContent}; </span>

这带来了Electron和我们刚刚实现的两个模块的必要功能。

<span style="color:rgba(0, 0, 0, 0.84)">  function createWindow(){ 
  mainWindow = new BrowserWindow({ 
 高度:563,useContentSize:true, 
  width:1000,webPreferences:{backgroundThrottling:false} 
  }); 
  mainWindow.loadURL(winURL); 
  if(process.env.NODE_ENV ==='development'){ 
  mainWindow.webContents.openDevTools(); 
  } 
  createServer(); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  mainWindow.on('closed',()=> { 
  mainWindow = null 
  }); 
  } </span>

createWindow函数就是我们顾名思义创建主窗口的地方。 当Electron过程启动时, app.on('ready')app.on('activate')处理程序会自动调用此函数。

BrowserWindow对象是在Electron中包装Chromium窗口的对象。 它是用于所有意图和目的的Web浏览器,我们将用它来托管我们的应用程序代码。

接下来的两行表明这是一个Web浏览器。 我们首先让BrowserWindow访问一个URL。 该URL的计算取决于应用程序是否在开发模式下运行。 在开发模式中,它使用与Webpack服务相对应的URL,否则它引用已部署应用程序中的位置。

其次,我们会自动打开Chrome开发者工具。 请记住,这是Electron给我们的一个巨大优势,因为我们可以访问世界级的开发人员工具台,用它来检查应用程序的HTML + CSS + JavaScript代码。 在开发模式下运行时禁用此功能。

最后,我们调用createServer来初始化预览服务器。

最后,在底部添加这两个功能。

<span style="color:rgba(0, 0, 0, 0.84)">  ipcMain.on('newContentToRender',function(event,content){ 
  const rendered = renderContent(content,'layout1.html'); 
  const previewURL = newContent(render); 
  mainWindow.webContents.send('newContentToPreview',previewURL); 
  }); </span>
<span style="color:rgba(0, 0, 0, 0.84)">  process.on('unhandledRejection',(reason,p)=> { 
  console.error(`未处理的拒绝:$ {util.inspect(p)}原因:$ {reason}`); 
  }); </span>

newContentToRender事件处理程序是我们将Markdown呈现为HTML并在PreviewIframe显示它的PreviewIframe 。 我们调用renderContent来呈现文本,然后通过调用newContent函数通知预览服务器,最后将预览URL发送到Renderer进程。 如果您回头一下,您会记得newContentToPreview消息由PreviewIframe组件处理,并导致<iframe>重新加载给定的URL。

unhandledRejection处理程序很重要,在Node.js的更高版本中将成为必需。此事件是在拒绝状态下为Promises发出的,但没有代码捕获拒绝Promise。换句话说, unhandledRejection是未被捕获的错误。 显然很糟糕的是没有捕获到你的错误,并且Node.js计划在将来的版本中通过使应用程序退出此事件来强制执行此操作。 因此,我们都必须习惯于添加此处理程序,以便我们可以警告我们未被捕获的错误。

顺便说一下,代码发出console.error每个地方都应该在应用程序中添加一条可见的警告消息。

启动应用程序

我们现在有足够的代码来运行编辑器应用程序。 它将无法打开文件或保存文件,但我们将能够使用编辑器,查看预览输出,并使用开发人员工具。

首先运行这个:

<span style="color:rgba(0, 0, 0, 0.84)">  $ npm install buefy ejs markdown-it  -  save 
  $ npm安装 </span>

我们添加了几个包,因此我们需要将它们添加到package.json中,然后重新安装。

<span style="color:rgba(0, 0, 0, 0.84)">  $ npm run dev </span>

这将以开发人员模式运行应用程序。 如果你检查生成的package.json,你会看到许多其他可以运行的脚本,我们稍后会介绍它们。 在任何情况下,在进行一些编辑后,我们最终会得到一个如下所示的窗口:

这是一个相当大的进步,但我们有一些明显缺少功能。 对于初学者:

  • 系统菜单是Electron提供的默认设置
  • 可以通过在顶部添加工具栏按钮来改进应用程序。
  • 它需要保存呈现的HTML。
  • 生成可安装的应用程序。

让我们在后续章节中处理这些任务。

应用菜单

Electron提供了一种非常简单的机制来指定系统菜单,甚至可以正确地与不同的Windows和Mac菜单范例正确集成。 一个创建一个menuTemplate对象,交给Electron,然后安装菜单项。

src/main创建一个名为mainMenu.js的新文件,其中包含:

<span style="color:rgba(0, 0, 0, 0.84)"> 从'电子'导入{app,Menu,dialog}; 
 从'./preview-server.js'导入{currentContent}; 
 从'fs-extra'导入fs; </span>
<span style="color:rgba(0, 0, 0, 0.84)">  export function mainMenu(mainWindow){ 
  const menuTemplate = [{ 
  label:'文件', 
 子菜单:[ 
  { 
 标签:'新', 
 加速器: 
  process.platform ==='darwin'?  'Command + N':'Ctrl + N', 
  click:()=> {mainWindow.webContents.send('newFile2Edit');  } 
  }, 
  { 
 标签:'打开', 
 加速器: 
  process.platform ==='darwin'?  'Command + O':'Ctrl + O', 
  click:()=> {mainWindow.webContents.send('openNewFile');  } 
  }, 
  { 
 标签:'保存', 
 加速器: 
  process.platform ==='darwin'?  'Command + S':'Ctrl + S', 
  click:()=> {mainWindow.webContents.send('saveCurrentFile');  } 
  }, 
  { 
  label:'导出为HTML', 
  click:async()=> { 
  let filename =等待新的Promise((resolve,reject)=> { 
  dialog.showSaveDialog({ 
 标题:“导出为HTML” 
  },filename => { 
 的console.log( 
  `导出到HTML GOT SAVE TO $ {filename}`); 
  if(filename){ 
 解决(文件名); 
  } else { 
 解析(未定义); 
  } 
  }); 
  }); 
  if(filename){ 
 等待fs.writeFile(文件名, 
  currentContent(),'utf8'); 
  } 
  } 
  }, 
  { 
 标签:'退出', 
 加速器: 
  process.platform ==='darwin'?  'Command + Q':'Ctrl + Q', 
  click:()=> {app.quit();  } 
  } 
  ] 
  }, 
  { 
 标签:'编辑', 
 子菜单:[{ 
 标签:'撤消', 
 加速器:process.platform ==='darwin' 
  ?  'Command + Z':'Ctrl + Z', 
  click:()=> { 
  mainWindow.webContents.send( 'editorDoUndo');  } 
  }, 
  { 
 标签:'重做', 
 加速器:process.platform ==='darwin' 
  ?  'Command + Shift + Z':'Ctrl + Shift + Z', 
  click:()=> { 
  mainWindow.webContents.send( 'editorDoRedo');  } 
  }, 
  {type:'separator'}, 
  {role:'cut'}, 
  {role:'copy'}, 
  {role:'paste'}, 
  {role:'pasteandmatchstyle'}, 
  {role:'delete'}, 
  { 
  label:'全选', 
 加速器:process.platform ==='darwin' 
  ?  'Command + A':'Ctrl + A', 
  click:()=> { 
  mainWindow.webContents.send( 'editorSelectAll');  } 
  } 
  ] 
  }, 
  { 
 标签:'查看', 
 子菜单:[ 
  {role:'reload'}, 
  {role:'forcereload'}, 
  {role:'toggledevtools'}, 
  {type:'separator'}, 
  {role:'resetzoom'}, 
  {role:'zoomin'}, 
  {role:'zoomout'}, 
  {type:'separator'}, 
  {role:'togglefullscreen'} 
  ] 
  }, 
  { 
 角色:'窗口', 
 子菜单:[ 
  {role:'minimize'}, 
  {role:'close'} 
  ] 
  }]; 

  if(process.platform ==='darwin'){ 
  menuTemplate.unshift({ 
  label:app.getName(), 
 子菜单:[ 
  {role:'about'}, 
  {type:'separator'}, 
  {role:'services',子菜单:[]}, 
  {type:'separator'}, 
  {role:'hide'}, 
  {role:'hideothers'}, 
  {role:'取消隐藏'}, 
  {type:'separator'}, 
  { 
 角色:'退出', 
 加速器:process.platform ==='darwin' 
  ?  'Command + Q':'Ctrl + Q' 
  } 
  ] 
  }); 
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">  const mainMenu = Menu.buildFromTemplate(menuTemplate); 
  Menu.setApplicationMenu(MAINMENU); 
  }; </span>

这些是相当典型的菜单选择。 在许多情况下,菜单项处理程序将事件从Main进程发送到Renderer进程。 这需要在该过程中实现匹配的事件处理程序。 在其他情况下,菜单项使用内置处理程序。

导出到HTML功能是个例外。 此功能非常简单,可以像这样实现内联。 我们只需使用Electron Save As对话框询问用户文件名(请参阅https://electronjs.org/docs/api/dialog )。 如果用户点击CANCEL,那么我们将undefinedfilename ,否则我们将获得一个文件名,在这种情况下,我们只需将currentContent写入指定文件。

src/main/index.js将此行添加到createWindow函数:

<span style="color:rgba(0, 0, 0, 0.84)">  MAINMENU(主窗口); </span>

这样,菜单会在应用程序运行时进行设置。

要处理所有这些事件,请在src/renderer/components/EditorPage.vue添加此created函数:

<span style="color:rgba(0, 0, 0, 0.84)">  created:function(){ 
  messageBus。$ on('newFile2Edit',(targetWindow)=> { 
  this.newFile2Edit(targetWindow); 
  }); 
  messageBus。$ on('editorDoUndo',()=> { 
  this.editor.editor.undo(); 
  }); 
  messageBus。$ on('editorDoRedo',()=> { 
  this.editor.editor.redo(); 
  }); 
  messageBus。$ on('editorSelectAll',()=> { 
  this.editor.editor.selectAll(); 
  }); 
  messageBus。$ on('openNewFile',async(file2open)=> { 
 试试{this.openNewFile();  } catch(err){ 
  console.error(`openNewFile ERROR $ {file2open} $ {err.stack}`); 
  } 
  }); 
  messageBus。$ on('saveCurrentFile',()=> { 
 试试{this.saveCurrentFile();  } catch(err){ 
  console.error( 
  `saveCurrentFile ERROR $ {file2open} $ {err.stack}`); 
  } 
  }); 
  }, </span>

对于每个我们收到消息,将其路由到适当的位置,例如我们现在必须添加到EditorPage.vue一些方法。 在methods对象中,添加以下函数:

<span style="color:rgba(0, 0, 0, 0.84)">  editorChanged(输入){this.isChangedFile = true;  }, 
  askSaveFile(file2save){ 
 返回新的Promise((resolve,reject)=> { 
 这一点。$ dialog.confirm({ 
 标题:“保存文件?”, 
 消息:`$ {file2save}`, 
  cancelText:'不', 
  confirmText:'是', 
  onCancel :()=> {resolve(“cancel”);  }, 
  onConfirm :()=> {resolve(“confirm”);  } 
  }) 
  }); 
  }, 
  async saveContentToFile(file2save){ 
  return await fs.writeFile(file2save,this.input,'utf8'); 
  }, 
  saveAsGetFileName(){ 
  const remote = this。$ electron.remote; 
  const dialog = remote.dialog; 
 返回新的Promise((resolve,reject)=> { 
 尝试{ 
  dialog.showSaveDialog({ 
 标题:“保存” 
  },filename => {resolve(filename);  }); 
  } catch(err){reject(err);  } 
  }); 
  }, 
  async openNewFile(){ 
  if(this.isNewFile && this.isChangedFile){ 
 让doit = await this.askSaveFile('UNTITLED'); 
  if(doit ===“confirm”){ 
  let fileName = await this.saveAsGetFileName(); 
  try {await this.saveContentToFile(fileName);  } catch(e){ 
  console.error(`openNewFile saveContentToFile FAIL,因为$ {fileName} $ {e.stack}`); 
  } 
  } 
  } else if(this.isChangedFile){ 
  let doit = await this.askSaveFile(this.fileName); 
  } 
 让file2open =等待新的Promise((resolve,reject)=> { 
  const remote = this。$ electron.remote; 
  const dialog = remote.dialog; 
  dialog.showOpenDialog({ 
 属性:['openFile'], 
 标题:“打开文档”, 
 过滤器:[{ 
 名称:“Markdown文件”, 
 扩展名:[“md”] 
  }] 
  }, 
  filePaths => { 
  if(filePaths){ 
 决心(文件路径[0]); 
  } else resolve(undefined); 
  }); 
  }); 
  if(!file2open)返回; 
 等待新的Promise((resolve,reject)=> { 
  fs.readFile(file2open,'utf8',(err,text)=> { 
  if(err)拒绝(错误); 
 其他{ 
  this.isNewFile = false; 
  this.isChangedFile = false; 
  this.fileName = file2open; 
  this.input = text; 
 解决(); 
  } 
  }); 
  }); 
  }, 
  async saveCurrentFile(){ 
 让p; 
  let fileName; 
  if(this.isNewFile && this.isChangedFile){ 
  fileName = await this.saveAsGetFileName(); 
  if(!fileName)返回; 
  } else if(this.isChangedFile){ 
  fileName = this.fileName; 
 否则返回; 
  try {await this.saveContentToFile(fileName);  } catch(e){ 
  console.error(`openNewFile saveContentToFile FAIL,因为$ {fileName} $ {e.stack}`); 
  } 
  this.isNewFile = false; 
  this.isChangedFile = false; 
  this.fileName = fileName; 
  }, 
  async newFile2Edit(){ 
 让p; 
  if(this.isNewFile && this.isChangedFile){ 
 让doit = await this.askSaveFile('UNTITLED'); 
  if(doit ===“confirm”){ 
  let fileName = await this.saveAsGetFileName(); 
  try {await this.saveContentToFile(fileName);  } catch(e){ 
  console.error(`openNewFile saveContentToFile FAIL,因为$ {fileName} $ {e.stack}`); 
  } 
  } 
  } else if(this.isChangedFile){ 
  let doit = await this.askSaveFile(this.fileName); 
  if(doit ===“confirm”){ 
  try {await this.saveContentToFile(fileName);  } catch(e){ 
  console.error(`openNewFile saveContentToFile FAIL,因为$ {fileName} $ {e.stack}`); 
  } 
  } 
  } 
  this.isNewFile = true; 
  this.isChangedFile = false; 
  this.fileName = undefined; 
  this.layoutFileName = undefined; 
  this.input =“#hello”; 
  } </span>

editorChanged我们只是跟踪内容是否已更改。 此处维护的标志将在其余代码中使用。

使用askSaveFile每当我们需要询问是否保存当前文件时,我们都会使用一个便利函数。 这使用Buefy对话框来提问。 另一个便利函数saveContentToFile只是将内容(在输入对象中)保存到指定文件中。 而另一个saveAsGetFileName使用Electron File Save对话框来获取用于保存内容的文件名。

在这两个对话框之间,我们看到了两种创建对话框的方法。 在一种情况下,我们有Buefy对话框,而在另一种情况下,我们使用Electron对话框。 电子时,Buefy不提供文件打开或文件保存对话框。 通常,Electron对话框是从Main进程启动的,但是我们在这里从Renderer进程启动它们。 Electron支持remote对象,该对象支持从Renderer进程访问进程资源,如对话框。

openNewFile我们处理打开文件。 我们必须考虑当前编辑缓冲区是否是“新”文件,意味着它是否已经与文件关联,缓冲区是否已被修改。 每个考虑都会导致出现一组不同的对话框。 在第一阶段,代码确定是否保存当前编辑缓冲区,如果是,则保存它的文件名。 在第二阶段,它询问用户要打开哪个文件。 如果用户确实选择了文件,则会读取该文件并将其分配给数据对象。

通过为数据对象赋值,可以触发一系列副作用,例如使文本出现在编辑器中,然后将预览生成到<iframe> 。

saveCurrentFile我们只关心保存文件。 到目前为止,我们只支持Save ,而不是Save As 。 因此,我们唯一一次询问要保存的文件名是在编辑器尚未与文件关联时。 否则,程序只是将编辑器缓冲区保存到关联文件中。

newFile2Edit我们只关心打开一个新文件。 需要考虑的是现有的编辑器缓冲区是否已更改,以及如何将缓冲区保存到文件中。 如果缓冲区未与文件关联,则必须使用“ 另存为”对话框来请求文件名。

工具栏,状态栏

这种应用程序通常具有带便利按钮的工具栏和显示信息的状态栏。在这种情况下,我们可能需要一些Markdown快捷方式,我们当然希望显示当前文件名和编辑状态。

对于工具栏按钮,我们将使用之前加载的Material Design图标包。要使用它,请将以下代码添加到src/renderer/main.js

<span style="color:rgba(0, 0, 0, 0.84)">导入“vue-material-design-icons / styles.css” 
从“vue-material-design-icons / file-plus.vue” 
导入FilePlus 从“vue-material-design-icons / content-save.vue”导入ContentSave 
从“vue-material-design-icons / folder-open.vue” 
导入FolderOpen 从“vue-material-design-icons / content-cut.vue” 
导入ContentCut从“vue-material-design-icons / format- ” 导入FormatBold bold.vue“ 
import FormatItalic from”vue-material-design-icons / format-italic.vue“</span>
<span style="color:rgba(0, 0, 0, 0.84)">Vue.component(“content-save”,ContentSave); 
Vue.component(“file-plus”,FilePlus); 
Vue.component(“folder-open”,FolderOpen); 
Vue.component(“content-cut”,ContentCut); 
Vue.component(“format-bold”,FormatBold); 
Vue.component(“format-italic”,FormatItalic);</span>

vue-material-design-icons包为每个图标实现一个Vue组件。图标名称如https://materialdesignicons.com/所示。对于您要使用的每个组件,请导入它,然后Vue.component使用导入的对象和所需的标记名称进行调用。对于每个创建的全局Vue组件,可以在应用程序的任何位置使用。

src/renderer/components/EditorPage.vue更改<template>时添加一行按钮:

<span style="color:rgba(0, 0, 0, 0.84)">< <strong>div</strong> id =“wrapper”> 
< <strong>b-tooltip</strong> label =“New file”position =“is-right”> 
< <strong>button</strong> class =“button”@ click =“newFile2Edit”> 
< <strong>file-plus</strong> > </ <strong>file-plus</strong> >
  </ <strong>button</strong> > 
</ <strong>b-tooltip</strong> > 
< <strong>b-tooltip</strong> label =“保存文件”position =“is-right”> 
< <strong>button</strong> class =“button”@ click =“saveCurrentFile”> 
< <strong>content-save</strong> > </ <strong>content-save</strong> >
  </ <strong>button</strong> > 
</ <strong>b-tooltip</strong> > 
< <strong>b-tooltip</strong> label =“打开文件”position =“is-bottom”> 
< <strong>button</strong> class =“button”@ click =“openNewFile”> 
< <strong>folder-open</strong> > </ <strong>folder-open</strong> >
  </ <strong>button</strong> > 
</ <strong>b-tooltip</strong> > 
< <strong>b-tooltip</strong> label =“Cut”position =“is-bottom”> 
< <strong>button</strong> class =“button”@ click =“editorContentCut”> 
< <strong>content-cut</strong> > </ <strong>content-cut</strong> >
  </ <strong>button</strong> > 
</ <strong>b-tooltip</strong> > 
< <strong>b-tooltip</strong> label =“插入粗体”position =“is-bottom”> 
< <strong>button</strong> class =“button”@ click =“editorFormatBold”> 
< <strong>format-bold</strong> > </ <strong>format-bold</strong> >
  </ <strong>button</strong> > 
</ <strong>b-tooltip</strong> > 
< <strong>b-tooltip</strong> label =“Insert italic”position =“is-bottom”> 
< <strong>button</strong> class =“button”@ click =“editorFormatItalic”> 
< <strong>format-italic</strong> > </ <strong>format-italic</strong> >
  </ <strong>button</strong> > 
</ <strong>b-tooltip</strong> >
  .. 
  </ <strong>div</strong> > </span>

模板的其余部分将保持不变,我们将此行按钮添加到页面顶部。每个都有一个on-click事件处理程序,在大多数情况下,处理程序调用已经存在的函数。我们确实有一些新的处理函数要实现。

我们还添加了工具提示,使应用程序更友好一些。因为每个按钮只是使用图标,所以用户确实需要一些单词来与图标图像一起使用。

三个新的事件处理函数是:

<span style="color:rgba(0, 0, 0, 0.84)">editorContentCut(){ 
let selected = this.editor.editor.getSelection(); 
if(!selected.isEmpty()){ 
let selectedRange = this.editor.editor.getSelectionRange(); 
this.editor.editor.getSession()。
getDocument()。replace(selectedRange,'');
  } 
 这个。$ nextTick(()=> {this.editor.editor.focus();}); 
  }, 
editorFormatBold(){ 
let selected = this.editor.editor.getSelection(); 
if(!selected.isEmpty()){ 
let selectedRange = this.editor.editor.getSelectionRange(); 
let selectedText = this.editor.editor.getSession()。
getDocument()。getTextRange(selectedRange); 
this.editor.editor.getSession()。getDocument(). 
replace(
selectedRange,`** $ {selectedText} **`);
  } else { 
 this.editor.editor.insert( '** ** BOLD'); 
  } 
 这个。$ nextTick(()=> {this.editor.editor.focus();}); 
  }, 
editorFormatItalic(){ 
let selected = this.editor.editor.getSelection(); 
if(!selected.isEmpty()){ 
let selectedRange = this.editor.editor.getSelectionRange(); 
let selectedText = this.editor.editor.getSession()。
getDocument()。getTextRange(selectedRange); 
this.editor.editor.getSession()。getDocument(). 
replace(
selectedRange,`_ $ {selectedText} _`);
  } else { 
 this.editor.editor.insert( '_ Italic_'); 
  } 
 这个。$ nextTick(()=> {this.editor.editor.focus();}); 
  }, </span>

虽然我们并没有完全实现一套完整的工具栏按钮,但我们在这种应用程序中展示了一些有用的模式。通过这三个函数,我们将展示如何访问和修改Ace编辑器组件中的内容。

另一个细节是调用该focus方法。有人观察到你可能在编辑器中输入文本,然后用鼠标单击一个按钮,然后你想继续打字。由于focus按钮位于按钮上,键盘事件会触发更多按钮按下。通过更改焦点,焦点停留在编辑器中而不是转移到按钮。

接下来,我们需要调整窗口的布局以适应工具栏。

<span style="color:rgba(0, 0, 0, 0.84)"><style scoped> 
#wrapper { 
height:100%; 
最小高度:100%;
  } 
.button-bar { 
margin-bottom:0px!important; 
身高:40px;
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">.button-bar按钮,
.button-bar a.navbar-item { 
padding:0px;
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)"> #editor { 
 位置:绝对; 
上:40px; 
底部:0px; 
左:0px; 
右:0px;
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">#aceeditor { 
height:100%; 
最小高度:100%;
  } </span>
<span style="color:rgba(0, 0, 0, 0.84)">#previewor { 
margin-left:2px; 
身高:100%; 
最小高度:100%;
  } 
 </样式> </span>

我们在工具栏中引入了一些复杂性。以前的布局是通过设置height:100%;所有内容来完成的。但是在工具栏就位的情况下,编辑器部分位于窗口底部以下,并且滚动行为不太理想。

在经过大量研究寻找更现代的方法来完成这种布局之后,使用了这种旧的静态定位待机。<nav>尝试了Bulma支持的几种标记变体。相反,将工具栏静态定位在窗口顶部,具有固定高度,然后静态定位编辑器区域,使其顶部与工具栏的固定高度匹配。

到目前为止,应用程序已经取得了一些进展。我们现在有一个可以构建的功能工具栏。显然,我们可以轻松地向工具栏添加更多按钮,但我们在底部实现状态栏具有更高的优先级。

返回EditorPage.vue,将以下内容添加到模板的底部

<span style="color:rgba(0, 0, 0, 0.84)">< <strong>section</strong> class =“status-bar columns”> 
< <strong>span</strong> class =“status-item column tag is-info”> {{fileName}} </ <strong>span</strong> > 
< <strong>span</strong> class =“status-item column tag is-info”> 
{{input.length}}个字节</ <strong>span</strong> > 
< <strong>span</strong> class =“status-item column tag is-info”> 
{{isChangedFile? “已更改”:“”}} </ <strong>span</strong> > 
< <strong>span</strong> class =“status-item列标记is-info”> 
{{isNewFile? “新”:“”}} </ <strong>span</strong> > 
</ <strong>section</strong> ></span>

我们所做的就是使某些数据值显示出来。当值发生变化时,Vue.js将自动更新显示,无需再编写任何代码。我们使用Bulma标签元素进行有用的着色。

然后将该<style>部分更改为以下内容:

<span style="color:rgba(0, 0, 0, 0.84)"><style scoped> 
#wrapper {..} 
.button-bar {..} 
.button-bar button,
.button-bar a.navbar-item {..} 
#editor {
 位置:绝对; 
上:40px; 
底部:30px; 
左:0px; 
右:0px; 
保证金:0px;
  } 
#aceeditor {..} 
#previewor {..} 
.status-bar {
 位置:绝对; 
身高:30px; 
右:0px; 
左:0px; 
底部:0px; 
保证金:0px;
  } 
.status-bar .status-item { 
vertical-align:middle;
  } 
 </样式> </span>

继续使用相同的静态布局方法,我们status-bar将窗口的高度设置为30像素。这意味着编辑器的底部必须高出窗口底部30个像素。

我们现在在屏幕底部有一个状态栏。

生成可安装的应用程序

使用Electron构建的所有商业应用程序都可以作为整齐打包的文件提供,其安装方式与任何其他应用程序一样。有多少人使用Postman知道或关心它是使用Electron构建的?安装任何这些应用程序都不需要任何异常 - 只需下载安装程序,然后像在给定平台上那样运行它。

electron-vue框架使我们可以轻松创建此类安装包。

查看scripts部分,package.json您将看到几个名为“ build”的部分。因为我们一开始就配置了Electron-Vue使用electron-builder

要构建Mac包,请运行:

<span style="color:rgba(0, 0, 0, 0.84)"> $ npm run build </span>
<span style="color:rgba(0, 0, 0, 0.84)">> electron-vue-buefy-editor@0.0.0 build / Volumes / Extra / sourcerer / 004-electron / electron-vue-buefy-editor 
> node .electron-vue / build.js && electron-builder
  .. </span>
<span style="color:rgba(0, 0, 0, 0.84)">✔构建主要流程
✔构建渲染器流程</span>
<span style="color:rgba(0, 0, 0, 0.84)">  .. </span>
<span style="color:rgba(0, 0, 0, 0.84)">•构建目标= macOS zip arch = x64 file = build / electron-vue-buefy-editor-0.0.0-mac.zip 
•building target = DMG arch = x64 file = build / electron-vue-buefy-editor-0.0。 0.dmg 
•构建块映射blockMapFile = build / electron-vue-buefy-editor-0.0.0.dmg.blockmap</span>
<span style="color:rgba(0, 0, 0, 0.84)">  .. </span>
<span style="color:rgba(0, 0, 0, 0.84)">$ ls -l build / 
total 196760 
-rw-r  -  r  -  1 david admin 412 Jul 13 14:15 electron-builder.yaml 
-rw-r  -  r  -  1 david admin 49188099 7月13日14:17 electron-vue-buefy -editor-0.0.0-mac.zip 
-rw-r  -  r  -  @ 1 david admin 51490606 7月13日14:16 electron-vue-buefy-editor-0.0.0.dmg 
-rw-r -r-1 david admin 55383 Jul 13 14:16 electron-vue-buefy-editor-0.0.0.dmg.blockmap 
drwxr-xr-x 5 david admin 170 Jul 13 13:55 icons 
drwxr-xr-x 3 david admin 102 Jul 13 14:15苹果电脑</span>

由于它创建了一个DMG文件,让我们来看看。双击DMG文件打开一个熟悉的Finder窗口,支持安装应用程序:

我们可以直接在这里双击应用程序,然后启动应用程序。

Electron支持跨平台应用程序。我们刚刚证明我们可以在MacOSX上构建应用程序,但是Windows呢?

将相同的源代码添加到Windows计算机。安装Node.js,按照安装说明操作,按照打包说明操作,最后得到一个EXE文件,它是一个应用程序安装程序。双击该EXE,然后安装该应用程序。它将显示在“开始”菜单中,您可以启动该应用程序。您可以进入Windows控制面板,在已安装的应用程序列表中找到该应用程序。该应用程序看起来与我们之前显示的相同 - 除了菜单栏位于应用程序窗口的顶部,就像Windows上的标准一样。 Electron会自动处理许多细节。

结论

我们在本教程中已经走了很长的路,并开发了一个有用的示例应用程序。在此过程中,我们已经看到在Electron中组装Vue.js应用程序是多么容易,可以在多个桌面计算机环境中提供。

显然,这些部件可以放在一起,以满足您的任何应用需求。这取决于您,Electron可以相对轻松地在多个平台上提供高保真应用。如果不出意外,Atom,Visual Studio Code,Postman等基于电子的应用程序的成功证明了可能性。

https://blog.sourcerer.io/creating-a-markdown-editor-previewer-in-electron-and-vue-js-32a084e7b8fe

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值