Electron構建一個文件瀏覽器應用(二)


在前一篇文章我們已經學習到了使用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源碼查看


免責聲明!

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



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