Electron構建一個檔案瀏覽器應用(二)

龍恩0707發表於2019-06-30

在前一篇文章我們已經學習到了使用Electron來構建我們的檔案瀏覽器了基礎東西了,我們之前已經完成了介面功能和顯示檔案或資料夾的功能了,想看之前文章,請點選這個連結  。現在我們需要在之前的基礎上來繼續完成餘下的功能,我們之前的只完成了介面和顯示資料夾或檔案。那麼這篇文章我們需要完成如下功能:

1. 如果它是一個資料夾,我們可以對該資料夾進行雙擊,然後開啟該資料夾。
2. 當前資料夾就是剛剛我們雙擊的那個資料夾。
3. 如果它內部有子資料夾的時候,我們也可以雙擊,然後重複第一步的操作步驟。

那麼在完成這些功能之前,我們先來整理一下我們的js檔案,那麼在整理之前,我們先來看看我們專案的整個目錄架構是一個什麼樣,這樣使我們更加的清晰。並且瞭解下我們各個檔案的程式碼及作用,哪些檔案具體做哪些事情的。這有助於我們更進一步來講解其他方面的知識,使下面講解的內容更通俗易解。

下面是我們的整個目錄架構的結構如下:

|----- 專案的根目錄
|  |--- image                # 存放資料夾或檔案圖示
|  |--- node_modules         # 所有的依賴包
|  |--- .gitignore           # github排除檔案
|  |--- app.css              # css檔案
|  |--- app.js               # 功能實現程式碼的檔案
|  |--- index.html           # html頁面
|  |--- main.js              # electron介面啟動程式碼
|  |--- package.json         

如上就是我們在第一篇文章中專案目錄結構,我們首先來看下我們的main.js 程式碼,該js檔案最主要的是 啟動electron桌面應用的程式,我們在package.json已經預設指定了該js檔案就是我們預設要載入的檔案。

package.json 程式碼如下:

{
  "name": "electron-filebrowser",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "async": "^3.1.0",
    "fs": "0.0.1-security",
    "osenv": "^0.1.5",
    "path": "^0.12.7"
  }
}

然後我們來看下我們的main.js 檔案程式碼,該檔案的程式碼最主要的是 處理electron介面的啟動,如下程式碼所示:

'use strict';

// 引入 全域性模組的 electron模組
const electron = require('electron');

// 建立 electron應用物件的引用

const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

// 定義變數 對應用視窗的引用 
let mainWindow = null;

// 監聽視窗關閉的事件(在Mac OS 系統下是不會觸發該事件的)
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// 將index.html 載入應用視窗中
app.on('ready', () => {
  /*
   建立一個新的應用視窗,並將它賦值給 mainWindow變數。
  */
  mainWindow = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true
    }
  });

  // 新增如下程式碼 可以除錯
  mainWindow.webContents.openDevTools();

  // 載入 index.html 檔案
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // 當應用被關閉的時候,釋放 mainWindow變數的引用
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

app.js 程式碼如下:

'use strict';

// 在應用中載入node模組
const fs = require('fs');
const osenv = require('osenv');

// 引入 aysnc模組
const async = require('async');
// 引入path模組
const path = require('path');

function getUsersHomeFolder() {
  return osenv.home();
}
// 使用 fs.readdir 來獲取檔案列表
function getFilesInFolder(folderPath, cb) {
  fs.readdir(folderPath, cb);
}

function inspectAndDescribeFile(filePath, cb) {
  let result = {
    file: path.basename(filePath),
    path: filePath,
    type: ''
  };
  fs.stat(filePath, (err, stat) => {
    if (err) {
      cb(err);
    } else {
      if (stat.isFile()) { // 判斷是否是檔案
        result.type = 'file';
      }
      if (stat.isDirectory()) { // 判斷是否是目錄
        result.type = 'directory';
      }
      cb(err, result);
    }
  });
}

function inspectAndDescribeFiles(folderPath, files, cb) {
  // 使用 async 模組呼叫非同步函式並收集結果
  async.map(files, (file, asyncCB) => {
    const resolveFilePath = path.resolve(folderPath, file);
    inspectAndDescribeFile(resolveFilePath, asyncCB);
  }, cb);
}

function displayFile(file) {
  const mainArea = document.getElementById('main-area');
  const template = document.querySelector('#item-template');
  // 建立模板實列的副本
  let clone = document.importNode(template.content, true);
  
  // 加入檔名及對應的圖示
  clone.querySelector('img').src = `images/${file.type}.svg`;
  clone.querySelector('.filename').innerText = file.file;

  mainArea.appendChild(clone);
}

// 該函式的作用是顯示檔案列表資訊
function displayFiles(err, files) {
  if (err) {
    return alert('sorry, we could not display your files');
  }
  files.forEach(displayFile);
}

/*
 該函式的作用是:獲取到使用者個人資料夾的路徑,並獲取到該資料夾下的檔案列表資訊
*/
function main() {
  const folderPath = getUsersHomeFolder();

  getFilesInFolder(folderPath, (err, files) => {
    if (err) {
      console.log('對不起,您沒有載入您的home folder');
    }
    console.log(files);
    /*
    files.forEach((file) => {
      console.log(`${folderPath}/${file}`);
    });
    */
    inspectAndDescribeFiles(folderPath, files, displayFiles);
  });
}

window.onload = function() {
  main();
};

如上app.js 程式碼就是我們頁面的功能程式碼,程式碼看起來有點混亂,因此我們需要把該檔案中的程式碼分離出來。也就是說之前我們所有功能性的程式碼都放在我們的app.js程式碼裡面,這樣以後業務越來越複雜的時候,js程式碼將來會越來越臃腫,因此現在我們需要把該檔案的功能性程式碼邏輯拆分開來。因此我們需要把它分成三個js檔案,app.js, fileSystem.js, userInterface.js.

app.js 還是負責入口檔案。
fileSystem.js 負責處理對使用者計算機中的檔案或資料夾進行操作。
userInterface.js 負責處理介面上的互動。

因此 fileSystem.js 程式碼如下:

'use strict';

const fs = require('fs');
// 引入 aysnc模組
const async = require('async');
// 引入path模組
const path = require('path');
const osenv = require('osenv');

function getUsersHomeFolder() {
  return osenv.home();
}

// 使用 fs.readdir 來獲取檔案列表
function getFilesInFolder(folderPath, cb) {
  fs.readdir(folderPath, cb);
}

function inspectAndDescribeFile(filePath, cb) {
  let result = {
    file: path.basename(filePath),
    path: filePath,
    type: ''
  };
  fs.stat(filePath, (err, stat) => {
    if (err) {
      cb(err);
    } else {
      if (stat.isFile()) { // 判斷是否是檔案
        result.type = 'file';
      }
      if (stat.isDirectory()) { // 判斷是否是目錄
        result.type = 'directory';
      }
      cb(err, result);
    }
  });
}

function inspectAndDescribeFiles(folderPath, files, cb) {
  // 使用 async 模組呼叫非同步函式並收集結果
  async.map(files, (file, asyncCB) => {
    const resolveFilePath = path.resolve(folderPath, file);
    inspectAndDescribeFile(resolveFilePath, asyncCB);
  }, cb);
}

module.exports = {
  getUsersHomeFolder,
  getFilesInFolder,
  inspectAndDescribeFiles
};

fileSystem.js檔案把我們之前的app.js中的 getUsersHomeFolder(), getFilesInFolder(), inspectAndDescribeFile(),
及 inspectAndDescribeFiles() 函式分離出來了,並且使用 module.exports 對暴露了 getUsersHomeFolder(), getFilesInFolder(), inspectAndDescribeFiles() 這三個函式。

userInterface.js 檔案中的程式碼如下:

'use strict';

let document;

function displayFile(file) {
  const mainArea = document.getElementById('main-area');
  const template = document.querySelector('#item-template');
  // 建立模板實列的副本
  let clone = document.importNode(template.content, true);
  
  // 加入檔名及對應的圖示
  clone.querySelector('img').src = `images/${file.type}.svg`;
  clone.querySelector('.filename').innerText = file.file;

  mainArea.appendChild(clone);
}

// 該函式的作用是顯示檔案列表資訊
function displayFiles(err, files) {
  if (err) {
    return alert('sorry, we could not display your files');
  }
  files.forEach(displayFile);
}

function bindDocument (window) {
  if (!document) {
    document = window.document;
  }
}

module.exports = {
  bindDocument,
  displayFiles
};

在userInterface.js中我們暴露了 bindDocument 和 displayFiles 兩個函式,bindDocument該函式的作用是將window.document 上下文傳遞進去,displayFiles函式的作用是將所有的檔案顯示出來。

接下來就是我們的app.js 程式碼了,該檔案需要引入我們剛剛 fileSystem.js 和 userInterface.js 的兩個檔案,因此我們的app.js 檔案程式碼被簡化成如下程式碼:

'use strict';

const fileSystem = require('./fileSystem');
const userInterface = require('./userInterface');

/*
 該函式的作用是:獲取到使用者個人資料夾的路徑,並獲取到該資料夾下的檔案列表資訊
*/
function main() {
  // 把window上下文傳遞進去
  userInterface.bindDocument(window);

  const folderPath = fileSystem.getUsersHomeFolder();

  fileSystem.getFilesInFolder(folderPath, (err, files) => {
    if (err) {
      console.log('對不起,您沒有載入您的home folder');
    }
    fileSystem.inspectAndDescribeFiles(folderPath, files, userInterface.displayFiles);
  });
}

window.onload = function() {
  main();
};

index.html 程式碼和之前一樣不變,如下程式碼:

<html>
  <head>
    <title>FileBrowser</title>
    <link rel="stylesheet" href="./app.css" />
  </head>
  <body>
    <template id="item-template">
      <div class="item">
        <img class='icon' />
        <div class="filename"></div>
      </div>
    </template>
    <div id="toolbar">
      <div id="current-folder">
      </div>
    </div>
    <!-- 該div元素是用來放置要顯示的檔案列表資訊-->
    <div id="main-area"></div>
    <script src="./app.js" type="text/javascript"></script>
  </body>
</html>

最後我們在我們的專案根目錄中執行 electron . 一樣也可以看到之前一樣的介面,如下所示:

一:實現資料夾雙擊功能

那麼我們如上程式碼重構完成後,我們現在需要實現我們對資料夾雙擊的功能了,那麼需要實現該功能的話,我們需要在 userInterface.js 中新增如下幾個函式來處理這些事情。

1. 首先新增一個 displayFolderPath 函式,該函式的作用是更新介面中的當前資料夾路徑。
2. 還需要新增 clearView 函式,該函式的作用是:顯示在主區域中的當前資料夾中的檔案和清除資料夾。
3. 還需要新增一個 loadDirectory函式,該函式的作用是:根據指定資料夾的路徑,獲取計算機中該路徑下的檔案或資料夾資訊,
並將其顯示在應用介面的主區域中。
4. 修改displayFiles函式,該函式的作用是:在資料夾圖示上監聽事件來觸發載入該資料夾中的內容。

因此我們的 userInterface.js 程式碼就變成如下了:

'use strict';

let document;

// 引入 fileSystem.js 中的模組程式碼
const fileSystem = require('./fileSystem');

// 更新當前資料夾路徑的函式
function displayFolderPath(folderPath) {
  document.getElementById('current-folder').innerText = folderPath;
}

// 移除 main-area div元素中的內容
function clearView() {
  const mainArea = document.getElementById('main-area');
  let firstChild = mainArea.firstChild;
  while (firstChild) {
    mainArea.removeChild(firstChild);
    firstChild = mainArea.firstChild;
  }
}

// 更新文字框中資料夾路徑,並且更新主區域中的內容
function loadDirectory(folderPath) {
  return function (window) {
    if (!document) {
      document = window.document;
    }
    // 更新最上面的文字框中的資料夾路徑
    displayFolderPath(folderPath);
    fileSystem.getFilesInFolder(folderPath, (err, files) => {
      // 先清除主區域中的內容
      clearView();
      if (err) {
        throw new Error('sorry, you could not load your folder');
      }
      fileSystem.inspectAndDescribeFiles(folderPath, files, displayFiles);
    });
  }
}

function displayFile(file) {
  const mainArea = document.getElementById('main-area');
  const template = document.querySelector('#item-template');
  // 建立模板實列的副本
  let clone = document.importNode(template.content, true);
  
  // 加入檔名及對應的圖示
  clone.querySelector('img').src = `images/${file.type}.svg`;

  // 需要判斷如果該檔案是目錄的話,需要對目錄圖片繫結雙擊事件
  if (file.type === 'directory') {
    clone.querySelector('img').addEventListener('dblclick', () => {
      // 我們雙擊完成後,就需要載入該資料夾下所有目錄的檔案
      loadDirectory(file.path)();
    }, false);
  }

  clone.querySelector('.filename').innerText = file.file;

  mainArea.appendChild(clone);
}

// 該函式的作用是顯示檔案列表資訊
function displayFiles(err, files) {
  if (err) {
    return alert('sorry, we could not display your files');
  }
  files.forEach(displayFile);
}

function bindDocument (window) {
  if (!document) {
    document = window.document;
  }
}

module.exports = {
  bindDocument,
  displayFiles,
  loadDirectory
};

如上就是我們的 userInterface函式新增的程式碼,我們仔細看下也是非常簡單的程式碼,我相信大家都能夠理解掉了,首先 displayFolderPath 這個函式,該函式的作用是顯示我們 左上角文字框的路徑,比如如下所示的:

然後我們 clearView 這個函式,該函式的作用是:清除主區域中所有的檔案或資料夾。

最後就是 loadDirectory 這個函式,該函式首先會呼叫displayFolderPath函式,來更新我們的左上角輸入框檔案路徑。期次就是呼叫 fileSystem.inspectAndDescribeFiles 函式來重新渲染主區域中的所有檔案。

給這個 displayFile 函式,判斷當前目錄是不是資料夾,如果是資料夾的話,對該資料夾圖示繫結了雙擊事件,雙擊後我們又呼叫了 loadDirectory 函式,重新更新左上角輸入框資料夾路徑,並且重新渲染主區域中的內容。

現在我們需要修改app.js 中的程式碼了,讓他呼叫 userInterface.js 檔案中的loadDirectory函式。重新初始化主區域內容,且更新左上角的輸入框的資料夾路徑。因此我們的app.js 程式碼更改成如下所示:

'use strict';

const fileSystem = require('./fileSystem');
const userInterface = require('./userInterface');

/*
 該函式的作用是:獲取到使用者個人資料夾的路徑,並獲取到該資料夾下的檔案列表資訊
*/
function main() {
  // 把window上下文傳遞進去
  userInterface.bindDocument(window);

  const folderPath = fileSystem.getUsersHomeFolder();

  /*
  fileSystem.getFilesInFolder(folderPath, (err, files) => {
    if (err) {
      console.log('對不起,您沒有載入您的home folder');
    }
    fileSystem.inspectAndDescribeFiles(folderPath, files, userInterface.displayFiles);
  });
  */
  userInterface.loadDirectory(folderPath)(window);
}

window.onload = function() {
  main();
};

如上所有的檔案更改完成後,我們現在再來重啟我們的應用程式,在專案中的根目錄 執行 electron . 命運後即可重啟,當我們雙擊應用中的某個資料夾時,就能看到工具條中當前資料夾路徑傳送改變了,並且該資料夾下所有子目錄也會更新了。
首先我們看下我們頁面初始化的時候,如下所示:

然後當我們點選我 工作文件 資料夾時候,會看到會更新工具條中的路徑,並且子目錄也會得到更新了。如下所示:

二:實現快速搜尋

現在我們目錄中有很多很多檔案及資料夾,但是當我們想要找某個資料夾的時候,我們很不方便,因此我們現在需要一個搜尋框,我們只要搜尋下我們目錄下的某個檔案就能找到,因此我們現在需要這麼一個功能,因此第一步我們需要在我們應用專案中的右上角新增一個搜尋框。我們需要實現如下功能:

1. 在我們的工具條的右上角新增一個搜尋框。
2. 引入一個記憶體搜尋庫來對檔案或資料夾進行搜尋。
3. 將當前資料夾中的檔案和資料夾資訊加入搜尋索引。
4. 使用者開始搜尋時,會對主區域顯示的檔案進行過濾。

2.1 在工具條中增加搜尋框

首先我們需要在 index.html 中的 current-folder div元素後面插入如下程式碼:

<input type="search" id="search" results="5" placeholder="Search" />

因此html部分程式碼變成如下:

<div id="toolbar">
  <div id="current-folder">
  </div>
  <input type="search" id="search" results="5" placeholder="Search" />
</div>

然後我們在我們的 app.css 程式碼中加入如下樣式:

#search {
  float: right;
  padding: 0.5em;
  min-width: 10em;
  border-radius: 3em;
  margin: 2em 1em;
  border: none;
  outline: none;
}

加入後,我們再來執行下我們的應用程式,使用命令 electron . ,會看到如下圖所示:

2.2 引入一個記憶體搜尋庫

上面我們已經通過html+css在我們的工具條右側新增了一個搜尋框,現在我們要做的事情就是通過一個搜尋庫來對檔案或資料夾列表進行搜尋。值得幸運的是,網上已經有一款叫 Iunr.js 客戶端搜尋庫了,它支援對檔案或資料夾列表進行索引,我們可以通過索引進行搜尋。因此我們需要在我們專案中根目錄命令列中來安裝該模組了,如下npm命令安裝:

npm i lunr --save

現在我們需要在我們的專案根目錄下新建一個叫 search.js 檔案,該檔案最主要的作用是處理搜尋。

因此我們再來看下我們整個目錄架構變成如下這個樣子了:

|----- 專案的根目錄
|  |--- image                # 存放資料夾或檔案圖示
|  |--- node_modules         # 所有的依賴包
|  |--- .gitignore           # github排除檔案
|  |--- app.css              # css檔案
|  |--- app.js               # 功能實現程式碼的檔案的入口
|  |--- index.html           # html頁面
|  |--- main.js              # electron介面啟動程式碼
|  |--- fileSystem.js        # 處理檔案操作的js
|  |--- userInterface.js     # 處理應用程式介面的js
|  |--- search.js            # 處理檔案搜尋的js
|  |--- package.json      

想要了解 lunr 庫的使用方法,請看這篇文章(http://www.uedsc.com/lunr-js.html

因此我們的search.js 程式碼如下:

'use strict';

// 引入 lunr 包進來
const lunr = require('lunr');

let index;

// 重置搜尋的索引函式

function resetIndex() {
  index = lunr(function() {
    this.field('file');
    this.field('type');
    this.ref('path');
  });
}

// 新增對檔案的索引,用於後續的搜尋
function addToIndex(file) {
  index.add(file);
}

// 對一個指定的檔案進行查詢
function find(query, cb) {
  if (!index) {
    resetIndex();
  }
  const results = index.search(query);
  cb(results);
}

module.exports = {
  addToIndex,
  find,
  resetIndex
};

現在我們搜尋庫已經引入了,並且程式碼也編寫完成後,我們現在要做的事情就是如何來監聽我們的搜尋框的事件了,那麼需要使用的'keyup' 事件來監聽,因此我們需要在 userInterface.js 檔案中新增如下一個函式,並且把該函式暴露出去。如下程式碼:

// 監聽搜尋函式
function bindSearchField(cb) {
  document.getElementById('search').addEventListener('keyup', cb, false);
}

module.exports = {
  bindDocument,
  displayFiles,
  loadDirectory,
  bindSearchField
}

如上程式碼就是監聽搜尋框 的keyup的事件了,當我們每次搜尋的時候,滑鼠keyup的時候,就會觸發一個cb函式,那麼觸發cb函式的時候,我們需要獲取輸入框的值,然後把該值傳遞進去查詢。如果沒有值的話,那麼不進行檔案搜尋,如果有值的話,我們需要進行檔案搜尋,現在要實現這個搜尋,我們需要完成如下事情:

1. 當我們的搜尋框沒有值的時候,確保所有的檔案都顯示在主區域中。
2. 當搜尋框中有值的時候,我們需要根據該值進行查詢及過濾且顯示出來。
3. 當搜尋到某個資料夾的時候,我們需要將該資料夾的所有的內容顯示在主區域中,並且重置該索引值。
4. 當有新檔案要顯示在主區域中,需要將它新增到索引中。

因此首先我們需要在我們的 userInterface.js 檔案中需要引入我們的search.js ,引入後我們就可以訪問search模組了。

引入完成後,我們需要改js中的 loadDirectory 函式,該函式的作用我們之前也講解過,就是更新左側文字框的檔案路徑,並且更新主區域中的內容,因此在該函式內部,我們每次呼叫該函式的時候都需要重置搜尋索引,這樣做的目的是能實現只針對當前資料夾內容進行搜尋。因此loadDirectory函式程式碼改成如下:

// 引入search模組
const search = require('search');

// 更新文字框中資料夾路徑,並且更新主區域中的內容
function loadDirectory(folderPath) {
  return function (window) {
    if (!document) {
      document = window.document;
    }

    // 新增重置搜尋索引的函式呼叫
    search.resetIndex();

    // 更新最上面的文字框中的資料夾路徑
    displayFolderPath(folderPath);
    fileSystem.getFilesInFolder(folderPath, (err, files) => {
      // 先清除主區域中的內容
      clearView();
      if (err) {
        throw new Error('sorry, you could not load your folder');
      }
      fileSystem.inspectAndDescribeFiles(folderPath, files, displayFiles);
    });
  }
}

如上更改完成後,我們需要更改下 displayFile 函式,在該函式中新增如下功能:

1. 把檔案新增到搜尋索引中的程式碼。
2. 將檔案路徑儲存在圖片元素的data-filePath屬性中,這樣的話,在檔案過濾的時候,我們可以根據該屬性值來過濾或顯示元素。

因此 displayFile 函式程式碼變成如下:

function displayFile(file) {
  const mainArea = document.getElementById('main-area');
  const template = document.querySelector('#item-template');
  // 建立模板實列的副本
  let clone = document.importNode(template.content, true);
  
  // 將檔案新增到搜尋索引中
  search.addToIndex(file);

  // 將檔案路徑儲存在圖片元素的data-filePath屬性中
  clone.querySelector('img').setAttribute('data-filePath', file.path);

  // 加入檔名及對應的圖示
  clone.querySelector('img').src = `images/${file.type}.svg`;

  // 需要判斷如果該檔案是目錄的話,需要對目錄圖片繫結雙擊事件
  if (file.type === 'directory') {
    clone.querySelector('img').addEventListener('dblclick', () => {
      // 我們雙擊完成後,就需要載入該資料夾下所有目錄的檔案
      loadDirectory(file.path)();
    }, false);
  }

  clone.querySelector('.filename').innerText = file.file;

  mainArea.appendChild(clone);
}

如上displayFile函式的作用是把所有的資料夾顯示在主區域中,並且繫結了資料夾的雙擊事件,並且我們把檔案新增到索引中了,並且將檔案路徑儲存到圖片元素的 data-filePath屬性中。

現在我們需要新增一個函式用於處理在介面上顯示搜尋的結果的函式,該函式首先要獲取到主區域中顯示的檔案或資料夾的路徑,然後判斷該路徑是否滿足使用者在搜尋框中條件,如果滿足的話,直接過濾掉不滿足的條件,顯示出來,因此我們在 userInterface.js檔案中後面新增一個函式,比如叫 filterResults函式。

function filterResults(results) {
  // 獲取搜尋結果中的檔案路徑用於對比
  const validFilePaths = results.map((result) => {
    return result.ref;
  });
  const items = document.getElementsByClassName('item');
  for (let i = 0; i < items.length; i++) {
    let item = items[i];
    let filePath = item.getElementsByTagName('img')[0].getAttribute('data-filePath');
    // 檔案路徑匹配搜尋結果
    if (validFilePaths.indexOf(filePath) !== -1) {
      item.style = null;
    } else {
      item.style = 'display:none;'; // 如果沒有匹配到,則將其掩藏掉 
    }
  }
}

上面函式編寫完成後,我們還需要編寫一個函式用於處理重置過濾結果的情況,當我們搜尋框值為空的時候,我們需要呼叫該函式來顯示檔案出來。我們可以把該函式名叫 resetFilter函式。

function resetFilter() {
  const items = document.getElementsByClassName('item');
  for (let i = 0; i < items.length; i++) {
    items[i].style = null;
  }
}

函式編寫完成後,我們還需要將該兩個函式暴露出去,因此程式碼如下:

module.exports = {
  bindDocument,
  displayFiles,
  loadDirectory,
  bindSearchField,
  filterResults,
  resetFilter
};

userInterface.js 程式碼已經完成後,我們現在需要在我們的app.js上更新程式碼,現在我們的app.js檔案需要做如下事情:

1. 在介面上需要監聽搜尋框。
2. 將搜尋關鍵詞傳給Iunr搜尋工具。
3. 將搜尋工具處理完的結果顯示到介面上。

因此app.js 程式碼變成如下:

'use strict';

const fileSystem = require('./fileSystem');
const userInterface = require('./userInterface');
// 引入search模組
const search = require('./search');

/*
 該函式的作用是:獲取到使用者個人資料夾的路徑,並獲取到該資料夾下的檔案列表資訊
*/
function main() {
  // 把window上下文傳遞進去
  userInterface.bindDocument(window);

  const folderPath = fileSystem.getUsersHomeFolder();

  // 更新文字框中資料夾路徑,並且更新主區域中的內容, 並且重置搜尋索引的函式呼叫
  userInterface.loadDirectory(folderPath)(window);
  // 監聽搜尋框值的變化
  userInterface.bindSearchField((event) => {
    const val = event.target.value;
    if (val === '') {
      // 如果搜尋框中的值為空的情況下, 重置過濾結果
      userInterface.resetFilter();
    } else {
      /*
       如果搜尋框中有值的話,將該值傳遞到搜尋模組的find函式處理並過濾結果顯示在介面上
      */
      search.find(val, userInterface.filterResults);
    }
  });
}

window.onload = function() {
  main();
};

現在我們在我們檔案的根目錄搜尋 tuge 的內容的話,可以看到如下所示的過濾:如下所示:

然後現在我們把搜尋條件清空的話,我們又可以看到所有的目錄了,如下所示:

現在我們再繼續點選 工作文件,進入該目錄後,我們在該目錄下繼續搜尋 18 這樣的時候,我們可以看到如下所示:

我們接著清空內容後,我們就可以看到 工作文件 目錄下的所有內容了,如下圖所示:

三:新增後退功能

如上我們已經實現了檔案或資料夾搜尋功能及顯示使用者資料夾的詳細路徑,並且還可以雙擊資料夾進入內部的功能,現在我們還需要實現後退功能,我們雙擊完成後進入資料夾內部,我們這個時候想後退的話不能後退,因此我們現在要實現這樣的功能。

想實現回退功能,我們又沒有和瀏覽器那樣有後退按鈕,因此我們這邊想實現後退功能,我們可以點選資料夾路徑後退即可,也就是說我們需要實現每個資料夾路徑可點選功能。比如如下圖對應的路徑點選即可:

實現當前資料夾路徑可單擊

如上圖一串路徑,我們希望點選某一個路徑的時候,希望切換到對應的資料夾那個地方,並且顯示資料夾下的所有內容,就像網頁連結一樣的。我們看下我們左側的路徑的原始碼可以看到,它是一個div元素顯示路徑的,如下圖所示:

我們來看下我們的工具條上顯示當前資料夾路徑的程式碼,在我們的userInterface.js中,有個函式,如下程式碼:

// 更新當前資料夾路徑的函式
function displayFolderPath(folderPath) {
  document.getElementById('current-folder').innerText = folderPath;
}

把路徑賦值給id為current-folder元素上,現在我們需要做的是,不再是把路徑賦值該div元素上,而是希望把該路徑傳遞給另一個函式去,然後該函式使用split分隔符分割('/')這樣的變成一個陣列,然後遍歷這個陣列,把它對應的檔名使用span標籤,也就是每個資料夾路徑使用span標籤包圍起來,並且在該span標籤上設定一個屬性,比如叫 data-path 這樣的,該屬性值就是該資料夾的具體路徑,現在我們需要做這些事情了。

我們需要接受folderPath路徑字串的函式,比如我們現在叫 convertFolderPathIntoLinks 這個函式,把它放到我們的 userInterface.js 中,該函式最主要做的事情就是分割路徑,並且把各個路徑使用span標籤包圍起來,因此我們需要引用path模組進來,

const path = require('path');

在Mac OS 和 linux中,路徑分隔符是斜槓(/), 但是在windows中,它是反斜槓(\), 因此我們需要使用path模組的 path.sep 來獲取分隔符。

convertFolderPathIntoLinks函式程式碼如下:

function convertFolderPathIntoLinks(folderPath) {
  const folders = folderPath.split(path.sep);
  const contents = [];
  let pathAtFolder = '';
  folders.forEach((folder) => {
    pathAtFolder += folder + path.sep;
    const str = `<span class="path" data-path="${pathAtFolder.slice(0, -1)}">${folder}</span>`;
    contents.push(str);
  });
  return contents.join(path.sep).toString();
}

如上函式接收 資料夾的路徑作為引數,我們會根據該路徑上的分隔符將其變為一個包含路徑上檔名的列表,有該列表,我們就可以對其中每個資料夾名建立一個span標籤。每個span標籤包含一個名為path的類名以及一個data-path屬性儲存當前資料夾的路徑。最後資料夾的名字以文字的形式包含在span標籤中。我們把所有的span標籤存入一個陣列 contents中,最後使用 分隔符分割,把陣列的內容以字串的形式返回回來。

我們之前的 displayFolderPath 函式的程式碼如下:

// 更新當前資料夾路徑的函式
function displayFolderPath(folderPath) {
  document.getElementById('current-folder').innerText = folderPath;
}

現在我們要把程式碼改成如下了:

// 更新當前資料夾路徑的函式
function displayFolderPath(folderPath) {
  document.getElementById('current-folder').innerHTML = convertFolderPathIntoLinks(folderPath);
}

那麼這樣的話,我們的 div中的id為 current-folder 元素就包含span標籤了,並且每個span標籤上都有 data-path 這個屬性,我們再來執行下我們的程式碼可以看到如下所示:

span標籤我們已經弄好了,我們現在需要的是再增加一個函式,該函式的作用是用於監聽單擊資料夾名的操作,並將單擊的資料夾路徑傳遞給回撥函式,回撥函式接收到我們單擊的資料夾路徑後,將其傳遞給負責顯示資料夾內容的函式。我們把該函式名取為:bindCurrentFolderPath. 因此程式碼新增如下:

function bindCurrentFolderPath() {
  const load = (event) => {
    const folderPath = event.target.getAttribute('data-path');
    loadDirectory(folderPath)();
  }
  const paths = document.getElementsByClassName('path');
  for (var i = 0; i < paths.length; i++) {
    paths[i].addEventListener('click', load, false);
  }
}

// 更新當前資料夾路徑的函式
function displayFolderPath(folderPath) {
  document.getElementById('current-folder').innerHTML = convertFolderPathIntoLinks(folderPath);
  // 呼叫繫結事件
  bindCurrentFolderPath();
}

如上程式碼實現完成後,我們再來重新下我們的運用程式,就可以看到效果了,我們點選某一個資料夾進去的時候,再點選路徑上的某一個資料夾即可返回到上一個頁面,就可以看到效果了。

我們需要新增一個簡單的樣式,就是說就是當我們滑鼠游標懸停到span元素上的時候,將滑鼠游標顯示為手狀形式。我們需要在我們的app.css 新增如下程式碼:

span.path:hover {
  opacity: 0.7;
  cursor: pointer;
}

四:實現開啟其他的檔案(比如文字檔案,視訊及文件等)

我們之前所有的功能是針對資料夾來做的,現在我們還要針對檔案,圖片,視訊,文件等這些型別的檔案實現點選功能,要實現這些,我們需要實現單擊檔案的功能。
因此我們需要在我們的 userInterface.js 檔案中,在displayFile函式中,新增else程式碼;如下程式碼:

function displayFile(file) {
  const mainArea = document.getElementById('main-area');
  const template = document.querySelector('#item-template');
  // 建立模板實列的副本
  let clone = document.importNode(template.content, true);
  
  // 將檔案新增到搜尋索引中
  search.addToIndex(file);

  // 將檔案路徑儲存在圖片元素的data-filePath屬性中
  clone.querySelector('img').setAttribute('data-filePath', file.path);

  // 加入檔名及對應的圖示
  clone.querySelector('img').src = `images/${file.type}.svg`;

  // 需要判斷如果該檔案是目錄的話,需要對目錄圖片繫結雙擊事件
  if (file.type === 'directory') {
    clone.querySelector('img').addEventListener('dblclick', () => {
      // 我們雙擊完成後,就需要載入該資料夾下所有目錄的檔案
      loadDirectory(file.path)();
    }, false);
  } else {
    // 不屬於資料夾以外的檔案,比如文字檔案,文件等
    clone.querySelector('img').addEventListener('dblclick', () => {
      fileSystem.openFile(file.path);
    })
  }

  clone.querySelector('.filename').innerText = file.file;

  mainArea.appendChild(clone);
}

如上程式碼,我們的else裡面實現了除了資料夾以外的檔案也可以進行雙擊操作,並且在回撥函式中我們呼叫了 fileSystem.js 模組中的openFile函式,因此我們需要實現 openFile函式的程式碼。那麼該函式需要呼叫 shell API。shell API 能夠使用系統預設的應用開啟URL,檔案以及其他型別的文件。因此在fileSystem.js 檔案中,我們需要新增如下程式碼:

const shell = require('electron').shell;

function openFile(filePath) {
  // 呼叫shell API的openItem函式
  shell.openItem(filePath);
}

並且匯出函式中需要新增 openFile ,如下所示:

module.exports = {
  getUsersHomeFolder,
  getFilesInFolder,
  inspectAndDescribeFiles,
  openFile
};

現在我們基本功能已經完成了,我們現在還需要當我們滑鼠移動到檔案或資料夾的時候,我們需要讓他變成滑鼠的形式,因此我們需要在我們的app.css中加如下程式碼:

img:hover {
  opacity: 0.7;
  cursor: pointer;
}

現在我們所有的程式碼已經完成了,我們接下來重新啟動下我們的應用程式,在專案的根目錄中使用命令 electron . 即可開啟我們的應用程式了。我們可以雙擊點選不屬於資料夾的檔案,也可以使用我們的shell預設方式開啟檔案了。如下所示:

github原始碼檢視

相關文章