1. 程式人生 > >第四章-使用本機檔案對話方塊和幫助程序間溝通 | Electron實戰

第四章-使用本機檔案對話方塊和幫助程序間溝通 | 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的通道傳送和偵聽訊息。