第四章-使用本機文件對話框和幫助進程間溝通 | Electron實戰


本章主要內容:

  • 使用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的通道發送和偵聽消息。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM