第四章-使用本机文件对话框和帮助进程间沟通 | Electron实战

1,722 阅读26分钟

本章主要内容:

  • 使用Electron的dialog模块实现一个本机打开文件对话框
  • 促进主进程和渲染器进程之间的通信
  • 将功能从主进程暴露给渲染器进程
  • 使用Electron的remote模块从主进程导入功能到渲染器进程
  • 使用webContents模块将信息从主进程发送到呈现器进程,并使用ipcRenderer模块为来自主进程的消息设置监听器

在前一章中,我们为第一个Electron项目打下了基础,这是一个笔记应用程序,它从左窗格中取出Markdown,并在右窗格中将其呈现为HTML。我们设置了主进程并将其配置为生成一个呈现器。我们建立了package.json,安装了必要的依赖项,创建了主进程和呈现器进程,并布置了UI。我们还探索了使我们的应用程序看起来像桌面应用程序的方法,但是我们还没有添加一个传统web应用程序所不能做的功能。

现在,应用程序允许用户在Markdown视图中编写。当用户在Markdown视图中按下一个键,应用程序将自动呈现Markdown为HTML并在HTML视图中显示它。

在本章中,我们将添加触发本机文件对话框的功能,并从文件系统上的任何位置选择文本文件并将其加载到应用程序中。在这章的最后,渲染进程的浏览器窗口中的“打开文件”按钮将从主进程触发“打开文件”对话框。在此之前,有必要更深入地讨论一下如何在进程之间进行通信。我们从第3章的分支开始,可以在第三章代码找到它。本章末尾的代码可以在第四章代码-使用本机文件对话框和帮助进程间沟通中找到。或者,您可以下拉主分支并检出这两个分支中的任何一个。

git clone  https://github.com/sanshengshui/AUG

git checkout -f 第4章-使用本机文件对话框和帮助进 程间通讯

触发本机文件对话框

开始的一个简单方法是,当应用程序第一次启动并发出ready事件时,提示用户打开一个文件,如图4.1所示。在创建BrowserWindow实例之前,应用程序已经在侦听ready事件。本章稍后,我们将学习如何从UI触发此功能。在下一章中,我们还将学习如何从应用程序菜单中触发它。

图4.1 我们的应用程序将在启动时触发“打开文件”对话框。到本章结束时,此功能将被从UI触发对话框的功能所取代。

您可以使用Electron dialog模块创建本机对话框。将清单4.1中的代码添加到app/main.js中,就在需要其他Electron模块的地方。

列表4.1 导入dialog模块

const { app, BrowserWindow, dialog } = require('electron');

最终,应用程序能从多个位置触发文件打开功能。第一步是创建一个稍后要引用的函数,首先,将选择的文件名称打印到控制台。

列表4.2 创建一个getFileFromUser()函数: ./app/main.js

const getFileFromUser = () => {
    const files = dialog.showOpenDialog({ //触发操作系统的OpenFile对话框。我们还将不同配置参数的JavaScript对象传递给函数。
        properties: ['openFile'] //配置对象在“打开文件”对话框中设置不同的属性。
    });
    
    if (!files) { return; } //如果没有任何文件,请尽早从函数中返回。
    
    console.log(files);  //将文件打印到控制台
};

我们的getFileFromUser()函数是dialog.showOpenDialog()的一个包装器,我们可以在应用程序的多个地方使用而无需重复。它将触发dialog上的showOpenDialog()方法,并传递一个JavaScript对象,该对象具有不同的设置,我们可以根据需要进行调整。在JavaScript中,对象的键称为其属性。我们正在创建的对话框的某些特性需要传递给dialog.showOpenDialog()配置的对象属性。其中一个设置是对话框本身的属性,配置对象上的properties属性接受我们可以在对话框上设置的不同标志的数组。在本例中,我们只激活openFile标志,它表示此对话框用于选择要打开的文件,而不是选择多个目录或多个文件。其他可用的标志是openDirectorymultiselection

dialog.showOpenDialog()返回所选文件的名称,用户选择的路径数组存储在名为files的变量中。如果用户按下取消,如果我们试图在未定义的情况下调用文件的任何方法,dialog.showOpenDialog()将返回未定义的并中断。

必须在应用程序的某个地方调用getFileFromUser()来触发对话框。最终,它将从UI和应用程序菜单中调用。现在,一个方便的地方是应用程序中启动时,当应用程序模块触发它的ready事件时调用getFileFromUser()。如下面的清单所示,当我们的UI被配置为从渲染器进程中触发getFileFromUser()时,这个步骤将被删除。

列表4.3 在应用程序第一次准备好时调用getFileFromUser()

app.on('ready', () => {
    mainWindow = new BrowserWindow({ show: false });
    
    mainWindow.loadFile('app/index.html');
    
    mainWindow.once('ready-to-show', () => {
        mainWindow.show();
        getFileFromUser();	//当窗口准备显示时,我们将调用getFileFromUser(),getFileFromUser()在清单4.2中定义
    });
    
    mainWindow.on('closed', () => {
        mainWindow = null;
    });
});

当我们的应用程序启动并完全加载窗口时,用户将立即看到一个文件对话框,这将允许他们选择一个文件(参见图4.2)。我们最终从启动过程中删除这个函数调用,并将其分配给UI中的"Open File"按钮。

图4.2 Electron能够在其支持的每个操作系统中触发本机文件对话框。

在图4.3中,我们可以在终端中显示的"Open File"对话框中看到选择的结果。注意dialog.showOpenDialog()返回一个数组。如果在对话框的属性数组中激活多重选择,用户可以选择 多个文件。为了一致性,Electron总是返回一个数组。

图4.3 选择文件后,文件的完整路径将被记录到终端窗口中的控制台。


使用Node读取文件

dialog.showOpenDialog()返回一个数组,其中包含用户选择的文件的路径,但它并不代表我们阅读这些文件。根据构建的文件类型,我们可能希望以不同的方式处理打开文件。在这个应用程序中,文件的内容被读取并立即显示在UI中。当用户选择文件时,处理复制图像或将图像上载到外部服务的不同应用程序可能采用相反的方法。另外一个应用程序可能会在播放列表中添加一个大的电影供以后观看。在这种情况下,立即打开大文件是浪费时间。

Node提供了一组用于处理其标准库中的文件的工具。内置的fs库处理常见的文件系统操作,比如读取和写入文件,所以应该要求它位于app/main.js的顶部。

列表 导入Node的fs模块: ./app/main.js

const	 {	app,	BrowserWindow,	dialog	} 	= 	require('electron');
const fs = require('fs');	//引入Node fs库

app.on(	'ready', ()=> {...});	// 为清楚起见省略了代码。
                       
 const getFileFromUser = () => {
     const files = dialog.showOpenDialog(mainWindow, {
         properties: ['openFile']
     });
     
     if	(!files)	{return;}
     
     const file = files[0]; //从数组中取出第一个文件
     const content = fs.readFileSync(file).toString(); //从文件中读取,并将生成的缓冲区转换为字符串。
     
     console.log(content);
 }                      

在清单4.4中,应用程序一次只打开一个文件。files[0]dialog.showOpenDialog()中选择数组中的第一个和唯一文件路径。在fs.readFileSync(file)中,文件路径作为参数传递给fs.readFileSync()。Node不知道打开了什么类型的文件,所以fs.readFileSync()返回一个缓冲区对象。但是,我们知道,在这个特定的应用程序中,我们通常使用纯文本。我们将它转换为一个字符串,并将文件的内容记录到终端,如图4.4所示。

图4.4 文件的内容被记录到用户的终端。


确定打开文件对话框的范围

如图4.4所示,getFileFromUser()成功地将文本文件的内容记录到终端。但有一个问题,默认情况下,dialog.showOpenDialog()允许我们打开计算机上的任何文件,而不考虑准备处理什么类型的文件。图4.5显示了通过对话框打开图像文件而不是文本文件时的问题结果。

图4.5 如果用户选择非文本文件,函数将记录二进制数据。

许多桌面应用程序可以限制用户可以打开的文件类型,这也适用于用Electron构建的应用程序。我们的应用程序不适合打开音乐文件,所以我们可能不应该让用户选择mp3。可以将其他选项添加到传递给dialog.showOpenDialog()的配置对象中,以将对话框限制为我们白名单中的文件扩展名。

列表4.5 白名单特定的文件类型: ./app/main.js

const getFileFromUser  = exports.getFileFromUser   = () => {
    const files = dialog.showOpenDialog({
      properties: ['openFile'],
      filters: [ //filters属性允许我们指定应用程序应该能够打开那些类型的文件,并禁止不符合我们标准的任何文件。
        { name: 'Text Files', extensions: ['txt'] },
        { name: 'Markdown Files', extensions: ['md', 'markdown'] }
      ]
    });
  
    if  (!files)  { return; } 
    
    const file = files[0];
    const content = fs.readFileSync(file).toString();
    
    console.log(content);
  };

在清单中,我们向传递给dialog.showOpenDialog()的对象添加了第二个属性。在Windows中,对话框在下拉框中Markdown文件的名称,如图4.6所示。在macOS中,没有下拉菜单,但是我们不能选择没有任何扩展的图像,如图4.7所示。


在macOS中实现对话表

Electron应用被设计成跨平台的,者意味着它们可以再macOS、Windows和Linux上运行。Electron提供了与本地特性和APIs,这些特性和APIs存在于每个支持的操作系统中,但不存在于其他操作系统中。我们在前面为文件扩展名过滤器提供名称时就看到了这一点,这个名称出现在Windows中,但是macOS没有这个功能。Electron利用了这个特性,如果它是可用的,但它仍然在没有的情况下工作。

在macOS中,我们能够从窗口顶部从表格的形式显示对话框,而不是显示在窗口前面(清单4.6)。通过在配置对象之前传递对BrowserWindow实例的引用(我们已经将其存储在mainWindow中)作为dialog.showOpenDialog()的第一个参数,我们可以轻松地在Electron中创建这个UI。

图4.6 在Windows中,我们可以在不同类型的文件之间切换。

图4.7 macOS不支持在不同类型的文件之间切换,但允许我们选择filter选项定义的任何符合条件的文件。

列表4.6 在macOS中创建工作表对话框: ./app/main.js

const getFileFromUser  = exports.getFileFromUser   = () => {
    const files = dialog.showOpenDialog(mainWindow, { // 传递对BrowserWindow实例的引用对话框。showOpenDialog将导致macOS将对话框显示为从窗口标题栏向下的工作表。它对Windows和Linux没有影响。
      properties: ['openFile'],
      filters: [
        { name: 'Text Files', extensions: ['txt'] },
        { name: 'Markdown Files', extensions: ['md', 'markdown'] }
      ]
    });
  
    if (files) { return; }
    
    const file = files[0];
    const content = fs.readFileSync(file).toString();
    
    console.log(content);
  };

通过这个简单的更改,Electron现在将Open File对话框显示为一个工作表,该工作表从传递给方法的窗口下拉,如图4.8所示。

图4.8 在macOS中,打开文件对话框现在从菜单的标题栏下拉,而不是作为应用程序窗口前面的附加窗口出现。


促进进程间通信

我们已经编写了用于在主进程中选择和读取文件的所有代码。但是我们如何将文件的内容发送到渲染器进程呢?如何从UI中触发主进程中的getFileFromUser()函数?

在构建传统web应用程序时,我们必须处理类似的问题。这并不完全相同,因为所有的代码都在客户机的计算机上运行,但是考虑一下我们通常如何构建web应用程序,可以作为理解如何构造Electron应用程序的一个有用的比喻。 参见图4.9。

图4.9 Electron与传统web应用程序的职责划分

在web上,我们通常在以下两个地方编写代码: 在服务器上或在用户浏览器中运行的客户端代码。客户端代码呈现UI,它监听并处理用户操作,并更新UI以显示应用程序的当前状态。然而,我们对客户端代码所能做的事件是有限制的。正如我们在第一章中讨论的,我们不能读取数据库或文件系统。服务端代码在我们的计算机上运行,它可以访问数据库,它可以写入我们系统上的日志文件。

在传统的web应用程序中,我们通常使用HTTP之类的协议来促进客户机和服务端进程之间的通信。使用HTTP,客户机可以发送带有信息的请求,服务器接受此请求,适当地处理它,并向客户机发送响应。

在Electron应用程序中,情况有些不同。正如我们在前几章中讨论过的,Electron应用由多个进程组成: 一个主进程和一个或多个渲染进程。所有东西都在我们的计算机上运行,但是角色的分离与客户机-服务器模型类似。我们不使用HTTP在进程之间通信。相反,Electron提供了几个模块来协调主进程和渲染进程之间的通信。

我们的主进程负责与本机操作系统APIs进行连接,它负责生成渲染器进程、定义应用程序菜单和显示打开和保存对话框、注册全局快捷方式、从操作系统请求电源信息、以及更多。执行这些任务所需的模块在Electron仅在主进程中可用来实现这一点,如图4.10所示。

图4.10 Electron提供不同的模块给主进程和渲染进程。这些模块代表了Electron的代码功能,到您阅读本文时,这个列表可能还会增长,并且可能还不完整。我鼓励您访问文档以查看最新的和最棒的特性。

Electron只向每个进程提供其模块的一个子集,而不保留我们访问与Electron模块分离的Node的APIs。如果愿意,我们可以从渲染器进程访问数据库和文件系统,但是有一些令人信服的理由将这种功能保留在主进程中。我们可能有很多渲染器进程,但是我们总是只有一个主进程。从我们的众多的渲染器读取和写入文件系统可能会出现问题;一个或多个进程试图同时写入同一个文件,或者从一个文件中读取,而另一个渲染器进程正在重写该文件。

JavaScript中的一个给定进程在一个线程上执行我们的代码,并且一次只能做一件事。通过将这些任务委托给主进程,我们可以确信一次只有一个进程执行对给定文件或数据库的读写。其他任务遵循正常的JavaScript协议,在事件队列中耐心等待,直到主进程完成当前任务。

主进程处理调用本机操作系统APIs或提供文件系统访问的任务是有意义的,但是触发这些操作的UI在渲染器进程中调用。即使所有的代码都在同一台计算机上运行,我们仍然需要协调进程之间的通信,因为我们必须协调客户机和服务器之间的通信。

最近,出现了WebSockets和WebRTC等协议,它们允许客户机和服务器之间的双向通信,甚至客户机之间的通信,而不需要中央服务器来促进通信。当我们构建桌面应用程序时,我们通常不会使用HTTP或WebSockets,但是Electron有几种协调进程间通信的方法,我们将在本章开始探讨,如图4.11所示。

图4.11 实现打开文件按钮涉及协调渲染器进程和主进程。

我们的UI包含一个带有标签Open File的按钮。当用户单击此按钮时,我们的应用程序应该提供一个对话框,允许用户选择要打开的文件。在用户选择一个文件之后,我们的应用程序应该读取文件的内容,在应用程序的左窗格中显示它们,并在右窗格中呈现相应的HTML。

正如您可能已经猜到的,这需要我们在两者之间进行协调渲染器进程(单击按钮的地方)和主进程(负责显示对话框并从文件系统中读取所选文件)。读取文件之后,主进程需要将文件的内容发送回渲染器进程(下一个清单),以便分别在左窗格和右窗格中显示和呈现。

列表4.7 在渲染器进程中添加事件监听器

const marked = require('marked');

const markdownView = document.querySelector('#markdown');
const htmlView = document.querySelector('#html');
const newFileButton = document.querySelector('#new-file');
const openFileButton = document.querySelector('#open-file');
const saveMarkdownButton = document.querySelector('#save-markdown');
const revertButton = document.querySelector('#revert');
const saveHtmlButton = document.querySelector('#save-html');
const showFileButton = document.querySelector('#show-file');
const openInDefaultButton = document.querySelector('#open-in-default');

const renderMarkdownToHtml = (markdown) => {
  htmlView.innerHTML = marked(markdown, { sanitize: true });
};

markdownView.addEventListener('keyup', (event) => {
  const currentContent = event.target.value;
  renderMarkdownToHtml(currentContent);
});

openFileButton.addEventListener('click', () => { //选择一个更新的CSS框模型,它将正确地设置元素的宽度和高度
  	alert('You clicked the "Open File" button.');
});

首先将事件监听器添加到渲染器进程中的Open File按钮。有了事件监听器,就可以与主进程协调,触发前面创建的Open File对话框。


介绍remote模块

Electron提供了许多方便进程间通信的方法。第一个是remote模块-一种从渲染器进程到主进程执行进程间通信的简单方法。 remote模块(仅在呈现器进程中可用)通过镜像主进程中可访问的模块,充当主进程的代理。当我们访问任何这些属性时,远程模块还负责与主进程之间的通信。

如图4.12所示,remote模块有几个属性,这些属性与仅对主进程可用的模块重叠。在我们的渲染器进程中,我们可以引用remote模块,它提供了对主进程中的对象和属性的访问,如图4.13所示。

图4.12 remote模块提供对通常仅对主进程可用的模块的访问。

图4.13 remote模块提供对通常仅对主进程可用的模块的访问。

当我们调用remote对象上的方法或属性时,它向主进程发送同步消息,在主进程中执行,并将结果发送回渲染器进程。remote模块允许我们在主进程中定义功能,并且很容易使其对渲染器进程可用。


使用进程间通信触发Open File函数

应用程序现在可以触发“Open File”对话框并读取用户在主进程中选择的文件。我们还向进程中的Open File按钮添加了一个事件监听器。现在只需要使用我们前面讨论过的进程间通信技术将它们连接起来。

理解CommonJS引用系统

通过remote模块使用主进程的功能,我们需要利用Node的CommonJS模块系统向应用程序中的其他文件公开该功能。在本书中,我们使用了require从Electron,Node标准库和第三方库中提取功能,但这是我们第一次将其与我们的代码一起使用。让我们花几分钟回顾一下它是如何工作的。

Node的模块系统由2个主要的方法所组成:从其他来源获取功能的能力,以及导出功能供其他来源使用的能力。当我们需要来自其他资源的代码时,其他资源可以是我们编写的文件、一个第三方模块、一个Node模块或Electron提供的模块。我们在主进程和渲染进程的顶部都使用了Node的内置requrie函数

当我们需要一个模块时,我们究竟要导入什么?在Node中,我们显式地声明应该从模块导出什么功能,如清单4.8所示。这个函数在清单4.9中导入,Node中的每个模块都有一个名为exports的内置对象,它从一个空对象开始。当我们从另一个文件中需要导出对象时,添加到导出对象的任何内容都是可用的。

清单4.8 在Node导出一个函数: basic-math.js

exports.addTwo = n => n + 2;


清单4.9 在Node导入一个函数

const basicMath = require('./basic-math');

basicMath.addTwo(4); //返回6



从另一个进程引用功能

内置的require函数不能跨进程工作。当我们在渲染器进程中工作时,我们使用内置的require函数导入的任何功能都将是渲染器进程的一部分。当我们在主进程中工作时,我们需要的任何功能都将是主进程的一部分。但是当我们在渲染器进程中想要从主进程中获得功能时,会发生什么呢?

Electron的remote模块有它自己的require方法,在我们的渲染器进程中允许它从主进程获取功能。使用remote.require返回代理对象—类似于远程对象上的其他属性。Electron代表我们负责所有的进程间通信。

要实现本章开头所述的功能,主进程必须导出它的getFileFromUser()函数,以便我们可以将它导入到渲染器进程代码中。这个清单更新了app/main.js中的一行。

清单4.10 从渲染器进程中导出打开文件对话框的功能: ./app/main.js

const getFileFromUser  = exports.getFileFromUser   = () => { //除了在这个文件中创建一个常量外,我们还将它指定为exports对象的一个属性,该属性可以从其他文件(特别是渲染器进程)访问。
    const files = dialog.showOpenDialog(mainWindow, {
      properties: ['openFile'],
      filters: [
        { name: 'Text Files', extensions: ['txt'] },
        { name: 'Markdown Files', extensions: ['md', 'markdown'] }
      ]
    });
  
    if (files) { returun; } 
    
    const file = files[0];
    const content = fs.readFileSync(file).toString();
    
    console.log(content);
  };


代码接受我们创建的getFileFromUser()函数,并将其导出为exports对象上具有相同名称的属性。渲染进程需要引入Electron的 remote 模块,然后使用remote.require。从渲染器进程的主进程获取对getFileFromUser()函数的引用。这与清单4.11中内置的require函数不同,因为导入的代码是根据主进程计算的,而不是根据引入它的渲染器进程计算的。这需要四个步骤:

  1. 在渲染器进程中需要Electron。

  2. 存储对remote的引用。

  3. 使用remote.require请求主进程。

  4. 存储从主进程导出的getFileFromUser()函数的应用。

列表4.11 渲染器进程中需要主进程的功能: ./app/renderer.js

const { remote } = require('electron');
const mainProcess = remote.require('./main.js');


现在,我们可以在渲染器进程中调用从主进程导出getFileFromUser()函数。让我们替换事件监听器中的功能,以触发Open File对话框,而不是触发警报。

列表4.12 从UI触发主进程中的getFileFromUser(): ./app/ renderer.js

openFileButton.addEventListener('click', () => {
mainProcess.getFileFromUser();
});


如果我们启动Electron应用程序并单击“Open File”按钮,它将正确地触发“打开文件”对话框。有了这些,我们仍然只将文件记录到主进程中的控制台。为了完成我们的特性,主进程必须将文件的内容发送回呈现器进程,以便在我们的应用程序中显示。


将内容从主进程发送到渲染器进程

remote模块促进了渲染器进程访问主进程的能力,但是它不允许主进程访问渲染器进程。要将用户选择的文件内容发送回要在UI中呈现的渲染器进程的话,我们需要学习进程之间通信的另一种技术。

每个BrowserWindow实例都有一个名为webContents的属性,它存储一个对象,该对象负责在调用new BrowserWindow()时创建的web浏览器窗口。webContentsapp类似,因为它在渲染器进程中根据web页面的生命周期发出事件。

以下是一些不完整的事件列表,你可以在webContents对象上监听:

  • did-start-loading
  • did-stop-loading
  • dom-ready
  • blur
  • focus
  • resize
  • enter-full-screen
  • leave-full-screen

webContents还有许多方法,可以在渲染器进程中触发与主进程不同的函数。在前一章中,我们通过主进程使用mainWindow.webContents.openDevTools()在渲染器进程中打开了Chrome开发工具。mainWindow.loadURL('file://${__dirname}/ index.html')mainWindow.webContents.loadURL()的别名,它在应用程序首次启动时将HTML文件加载到渲染器进程中。图4.14显示了更多的别名。

图4.14 BrowserWindow实例的方法是Electron webContents API的别名。

webContents有一个名为send()的方法,它将信息从主进程发送到渲染器进程。webContents.send()接受可变数量的参数。第一个参数是用来发送消息的通道的名称,它是一个任意字符串。渲染器进程中的事件监听器在同一通道上监听。当我们看到它的行动时,这种流动将变得更加清晰。第一个参数之后的所有后续参数都传递给渲染器进程。


发送文件内容到渲染器进程

我们当前实现是读取用户选择的文件并打印到终端上,mainWindow.webContents.send()将文件的内容发送到渲染器进程中。下一章将介绍打开文件的其他方法,这些方法不需要一个对话框来提示用户选择特定的文件,因为我们确实会遇到一些情况,在不触发对话框的情况下打开文件。

列表4.13 从主进程发送内容到渲染器进程: ./app/main.js

const getFileFromUser  = exports.getFileFromUser   = () => {
    const files = dialog.showOpenDialog(mainWindow, {
      properties: ['openFile'],
      filters: [
        { name: 'Text Files', extensions: ['txt'] },
        { name: 'Markdown Files', extensions: ['md', 'markdown'] }
      ]
    });
  
    if (files) { openFile(files[0]); } // 在前面,在文件未定义的情况下,使用return语句中断了函数。在本例中,当dialog.showOpenFile()成功返回一个文件路径数组时,我们将调整逻辑并将第一个文件传递给Open File。
  };
  
  const openFile = (file) => {
    const content = fs.readFileSync(file).toString();
    mainWindow.webContents.send('file-opened', file, content); // 我们将通过"file-opened"通道将文件的名称及其内容发送到渲染器进程
  };


主进程现在通过打开的文件file-opened通道广播文件的名称及其内容。下一步是使用ipcRenderer模块在渲染器进程中file-opened通道上设置监听器。Electron提供了两个基本模块,用于在进程之间来回发送消息: ipcRendereripcMain。每个模块仅在与之共享名称的进程类型中可用。

ipcRender可以向主进程发送消息,最重要的是,它还可以监听使用webContents.send()从主进程发送的消息。它在渲染器进程中需要ipcRenderer模块。

列表4.14 导入ipcRenderer模块: ./app/renderer.js

const { remote, ipcRenderer } = require('electron'); //将在我们的渲染器进程中导入ipcRenderer模块
const mainProcess = remote.require('./main.js')


有了这些,我们现在可以设置一个监听器。ipcRenderer监听file-opened通道,将内容添加到页面,并将Markdown渲染为HTML

列表4.15 在file-opened通道上监听消息

ipcRenderer.on('file-opened', (event, file, content) => {
  markdownView.value = content;
  renderMarkdownToHtml(content);
});


ipcRenderer.on接受两个参数:要监听的参数和一个回调函数,回调函数定义当渲染器进程在设置监听器的通道上接受到消息时要采取的操作。回调函数在调用时提供几个参数,第一个是事件对象,它与浏览器中的普通事件监听器一样。它包含关于我们为其设置监听器事件的消息,其他参数是在主进程中使用webContents.send()时提供的。在清单4.13中,我们发送了文件的名称及其内容,这些将是传递给监听器的附加参数。

有了这些新增功能,用户现在可以单击Open File按钮,使用本机文件对话框选择一个文件,并在UI中呈现内容。我们已经成功地实现了我们在本章开始时设定的特性,我们的主进程和渲染进程的代码应该类似于以下两个清单。

列表4.16 在主进程实现打开文件的功能: ./app/main.js

const{ app, BrowserWindow,dialog } = require('electron');
const fs = require('fs');


let mainWindow = null;

app.on('ready', () => {
    
    mainWindow = new BrowserWindow({
        show: false,
        webPreferences: {
            nodeIntegration: true
        }
    })
    
    mainWindow.loadFile('app/index.html');

    mainWindow.once('ready-to-show', () => {
        mainWindow.show();
    });

    mainWindow.on('closed', () => {
 
        mainWindow = null;
    });
});

const getFileFromUser  = exports.getFileFromUser   = () => {
    const files = dialog.showOpenDialog(mainWindow, {
      properties: ['openFile'],
      filters: [
        { name: 'Text Files', extensions: ['txt'] },
        { name: 'Markdown Files', extensions: ['md', 'markdown'] }
      ]
    });
  
    if (files) { openFile(files[0]); } 
  };
  
  const openFile = (file) => {
    const content = fs.readFileSync(file).toString();
    mainWindow.webContents.send('file-opened', file, content); 
  };


列表4.17 打开文件功能实现: ./app/renderer.js

const { remote, ipcRenderer } = require('electron');
const mainProcess = remote.require('./main.js')

const marked = require('marked');

const markdownView = document.querySelector('#markdown');
const htmlView = document.querySelector('#html');
const newFileButton = document.querySelector('#new-file');
const openFileButton = document.querySelector('#open-file');
const saveMarkdownButton = document.querySelector('#save-markdown');
const revertButton = document.querySelector('#revert');
const saveHtmlButton = document.querySelector('#save-html');
const showFileButton = document.querySelector('#show-file');
const openInDefaultButton = document.querySelector('#open-in-default');

const renderMarkdownToHtml = (markdown) => {
  htmlView.innerHTML = marked(markdown, { sanitize: true });
};

markdownView.addEventListener('keyup', (event) => {
  const currentContent = event.target.value;
  renderMarkdownToHtml(currentContent);
});

openFileButton.addEventListener('click', () => {
  mainProcess.getFileFromUser();
});

ipcRenderer.on('file-opened', (event, file, content) => {
  markdownView.value = content;
  renderMarkdownToHtml(content);
});



总结

  • Electron提供了用于创建各种本机操作系统对话框的对话模块。
  • 打开对话框可以配置为允许一个文件或目录以及多个文件或目录。
  • 打开对话框可以配置为只允许用户选择特定的文件类型。
  • 打开对话框返回一个数组,该数组由用户选择的一个或多个文件或目录组成。
  • Electron不包括读取文件的能力,相反,我们使用Node的fs模块来读写文件系统。
  • 每个操作系统都提供了一组不同的功能。如果在给定的操作系统中不存在该特性,那么Electron将使用可用的特性,同时提供一个优雅的后备。
  • 在macOS中,我们可以通过在dialog. showopendialog()中提供对该窗口的引用作为第一个参数,使对话框从其中一个窗口作为工作表下拉。
  • 本机操作系统APIs和文件系统访问应该由主进程处理,而呈现UI和响应用户输入应该由渲染器进程处理。
  • Electron提供了一套不同的模块给主进程和渲染器进程。
  • remote模块为主进程模块和函数提供代理,并使该功能在渲染器进程中可用。
  • 我们可以使用webContents.send ()命令将消息从主进程发送到渲染器进程。
  • 我们可以使用ipcRenderer模块监听主进程发送渲染器进程的消息。
  • 我们可以使用通道来命名消息的名称空间,通道是任意字符串。在本章中,我们使用file-opened的通道发送和侦听消息。