代码改变世界

Electron构建一个文件浏览器应用(一)

2019-06-25 00:01 龙恩0707 阅读(...) 评论(...) 编辑 收藏

在window、mac、linux系统中,他们都有一个共同之处就是以文件夹的形式来组织文件的。并且都有各自的组织方式,以及都有如何查询和显示哪些文件给用户的方法。那么从现在开始我们来学习下如何使用Electron来构建文件浏览器这么一个应用。

注意:我也是通过看书,看资料来学习的。这不重要,重要的是我们学到东西。我们知道如何使用 electron 来做一个桌面型应用软件。有这些知识点后,以后我们做其他的桌面型应用软件会有基础。

那么既然是文件浏览器,那么我们可以给文件浏览器取一个名字,假如叫他为 FileBrowser. 那么该文件浏览器要具备如下功能:

1. 用户可以浏览文件夹和查找文件。
2. 用户可以使用默认的应用程序打开文件。

Electron应用它是以一个js文件作为入口文件的,所以呢我们需要和之前一篇文章讲的一样,我们需要看下有如下目录结构:

|------- FileBrowser
|  |--- main.js
|  |--- index.html
|  |--- package.json

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"
}

main.js 基本代码如下(和第一篇文章的实现hello world代码是一样的):

'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();

  // 载入 index.html 文件
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // 当应用被关闭的时候,释放 mainWindow变量的引用
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

然后我们的index.html 代码如下:

<html>
  <head>
    <title>FileBrowser</title>
  </head>
  <body>
    <h1>welcome to FileBrowser</h1>
  </body>
</html>

然后我们在项目的根目录下 运行 electron . 命运,即可打开应用窗口,如下所示:

现在我们需要实现如下这个样子的;如下所示:

1. 通过Node.js找到用户个人文件夹所在的路径

想要显示用户个人文件夹的路径,我们先得想办法获取到该路径,并且要支持window、mac、及linux系统。在mac系统中,用户个人文件夹在 /User/<username> , 这里的username是用户名(我这边是 /User/tugenhua), 在linux系统中,用户的个人文件夹位于 /home/<username>. 在window10中,则位于C盘的 /User/<username>. 因此不同的操作系统它处于的位置不同。

在Node.js 中有一个叫 osenv 模块即可解决如上不同位置的问题,有个函数 osenv.home()可以返回用户个人文件夹。
要使用该模块,我们可以先进行安装,当然我们也要安装fs模块,需要对文件操作,因此如下命令安装:

npm install osenv fs --save

因此我们现在需要在 main.js 中加上该模块的代码,最终main.js 变成如下代码:

'use strict';

// 引入 全局模块的 electron模块
const electron = require('electron');

// 在应用中加载node模块
const fs = require('fs');
const osenv = require('osenv');

function getUsersHomeFolder() {
  return osenv.home();
}
// 使用 fs.readdir 来获取文件列表
function getFilesInFolder(folderPath, cb) {
  fs.readdir(folderPath, cb);
}
/*
 该函数的作用是:获取到用户个人文件夹的路径,并获取到该文件夹下的文件列表信息
*/
function main() {
  const folderPath = getUsersHomeFolder();
  getFilesInFolder(folderPath, (err, files) => {
    if (err) {
      console.log('对不起,您没有加载您的home folder');
    }
    files.forEach((file) => {
      console.log(`${folderPath}/${file}`);
    });
  });
}

main();

// 创建 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();

  // 载入 index.html 文件
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // 当应用被关闭的时候,释放 mainWindow变量的引用
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

然后我们继续在命令行中运行:electron . ,然后我们会看到如下效果:

现在我们已经知道了如何获取用户个人文件夹下的文件列表了。现在我们要考虑的问题是:如何获取文件名及文件类型(是文件还是文件夹)。并将他们以不同的图标在界面上显示出来。

如上代码我们已经获取到了文件列表,现在我们可以以文件列表作为参数,将它传递给Node.js文件系统的API中的另一个函数,
该函数要做的事情是:能够识别是文件还是文件夹以及他们的名字和完整的路径。要完成这些事情,我们要做如下三件事:

1. 使用 fs.stat函数。来读取文件状态。
2. 使用 async模块来处理调用一系列异步函数的情况并收集他们的结果。
3. 将结果列表传递给另一个函数将他们显示出来。

因此我们首先要安装 async 模块,安装命令如下:

npm install async --save

因此我们的main.js 继续添加代码,代码变成如下:

'use strict';

// 引入 全局模块的 electron模块
const electron = require('electron');

// 在应用中加载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 displayFiles(err, files) {
  if (err) {
    return alert('sorry, we could not display your files');
  }
  files.forEach((file) => {
    console.log(file);
  });
}


/*
 该函数的作用是:获取到用户个人文件夹的路径,并获取到该文件夹下的文件列表信息
*/
function main() {
  const folderPath = getUsersHomeFolder();
  getFilesInFolder(folderPath, (err, files) => {
    if (err) {
      console.log('对不起,您没有加载您的home folder');
    }
    /*
    files.forEach((file) => {
      console.log(`${folderPath}/${file}`);
    });
    */
    inspectAndDescribeFiles(folderPath, files, displayFiles);
  });
}

main();

// 创建 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();

  // 载入 index.html 文件
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // 当应用被关闭的时候,释放 mainWindow变量的引用
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

保存完该 main.js 后,我们接着运行 electron . 命令即可在命令行窗口打印出 对象 {file: '', path: '', type: '' }这样的了,如下所示:

2. 视觉上显示文件和文件夹

在如上main.js 代码中,我们在该文件中有个函数 displayFiles ,我们可以继续在该函数内部处理将文件名以及对应的图标展示在界面上。由于要显示的文件比较多,因此我们会将每个文件定义一套模板,然后为每个文件创建一个该模板的实列再渲染到界面上。

首先我们在index.html文件中添加html模板,模板中包含一个div元素,在其中包含了要显示的文件信息,因此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>

如上代码 template模板元素的作用是:为每一个渲染的文件信息定义一套HTML模板,真正被渲染到 id为 main-area 元素上,它会将用户个人文件夹中的每个文件信息都显示出来。因此下面我们需要在我们的main.js中添加一些代码,用来创建模板实列并添加到界面上。为了把main.js 启动代码和业务代码分开,因此我们再新建一个app.js,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();
};

然后main.js 代码如下:

'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;
  });
});

如上代码是目前所有的代码了,我们运行下 electron . 命令后,可以看到如下所示:

如上图可以看到我们的代码有调试代码了,那是因为在main.js加上了如下这句代码:

// 添加如下代码 可以调试
mainWindow.webContents.openDevTools();

并且如果我们按照之前的代码,在main.js 实列化 BrowserWindow 的时候,如下实列化代码:

mainWindow = new BrowserWindow();

如下代码:

// 将index.html 载入应用视窗中
app.on('ready', () => {
  /*
   创建一个新的应用窗口,并将它赋值给 mainWindow变量。
  */
  mainWindow = new BrowserWindow();

  // 添加如下代码 可以调试
  mainWindow.webContents.openDevTools();

  // 载入 index.html 文件
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // 当应用被关闭的时候,释放 mainWindow变量的引用
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
});

在控制台中会报如下的错:

解决的方案就是加上如下配置:

mainWindow = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true
  }
});

这是因为最新的electron@5.0系列中,这个nodeIntegration参数,默认改成false了。
而在以前版本的electron中,这个nodeIntegration参数,默认为true。

github源码查看