如果您对本文标题的最初反应是类似? ,我想向您保证。 您不必相信我的话! 我要做的是向您展示如何构建可以在多个操作系统上运行,与之交互并以愉悦的方式呈现结果的优秀软件。 整个任务将通过使用JavaScript和少量的bash / powershell命令来完成。
话虽如此,您可能想知道为什么我要进行此实验。 这可能令人惊讶,但是“冬夜漫长而寂寞,我需要一些时间来消磨时间”并不是这个问题的答案。 也许“我想提高自己的技能并精通JS大师”这一行会更近一些。
尽管此项目本身并没有很高的价值,但我的拙见是,它将:
- 为您提供构建RESTful服务的技能(和一些基本设计)以及您喜欢的操作系统所需的任何界面
- 让您专注于跨OS兼容性
- 向您介绍JavaScript和有用的Node.js模块的有价值的设计模式。
考虑到这一点,让我们开始讨论服务器。 我们需要创建一个(RESTful)服务,以实时向我们提供来自操作系统的最新读数。
我们为什么需要服务器? 为什么要RESTful?
这两个聪明的问题的答案很简单。 首先,我们需要一台服务器,因为出于安全原因,浏览器无法让您在操作系统上执行命令(我敢打赌,如果任何令人毛骨悚然的网站能够删除您的所有文件,您都不会感到满意,你会?)。 其次,我们将提供一个RESTful服务,因为使用REST接口有很多优点。 这超出了我们的范围,但是在本文结尾处,我将为感兴趣的读者提供一些有用的资源,以了解有关此主题的更多信息。
现在,我们想要的是至少一个可以通过HTTP协议从任何服务调用的终结点,从而与它将提供的数据的实际表示分离开来,并作为响应将数据发送回调用方。
要发回该数据,我们当然需要在格式上达成共识。 我们可以发送一些原始文本并留给客户端解析,或者,可以发送结构化数据(例如,使用XML)。 我最终选择了JSON。 原因是我们将拥有结构化的数据,但冗余性远不及XML。 请注意,通过就数据格式达成一致,我们为客户端引入了某种耦合,现在必须遵守我们的格式。 但是,此选择具有几个优点:
- 我们可以在界面中指定格式:客户端自然必须遵守其使用的任何服务的API(例如,方法的名称或公开的端点),只要我们不更改格式,不会有什么区别。 显然,在发布版本1之前,我们仍然应该考虑这种格式。实际上,我们(几乎)永远都不要更改公共接口,以避免客户端被破坏。
- 我们将通过委派解析给他们明智地减慢客户端的速度。
- 通过为所有操作系统提供通用格式,我们可以将它们分离。 为了支持新的操作系统,我们所需要的只是一个适配器,用于接收从其接收的数据。
在这一点上,我们需要开始讨论如何以及在何处获取发送到客户端的数据。 这也许是游戏中最棘手的部分,但是幸运的是,Node.js有很多模块,这些模块使我们的服务器可以与我们的操作系统进行通讯,甚至可以了解机器上正在运行的操作系统。
创建端点
要创建服务的核心,我们需要使用Node.js的HTTP模块来处理传入的GET请求:
var http = require('http');
var PORT = 8080;
由于我们正在构建仅在本地主机上运行的应用程序,因此我们可以为端口使用静态(恒定)值。 另一种选择是从命令行读取它,然后在未提供该值时退回到恒定值。 我们可以从process.argv
读取命令行参数。 由于第一个参数始终是"node"
,第二个参数始终是我们正在运行的JavaScript文件的名称,因此我们对第三个参数感兴趣:
var PORT = Number(process.argv[2]) || 8080;
HTTP模块使创建服务器和侦听端口变得容易。 我们只需要使用模块中声明的两个函数createServer()
和listen()
。 前者将带有两个参数(请求及其响应)的回调作为输入,而后者仅采用我们需要监听的端口号。 我们要创建REST端点,因此我们需要检查已请求的路径。 此外,我们希望根据匹配的端点来执行不同的操作。 假设我们希望电池信息的路径为/battery
。 为了允许小的变化(例如/battery/
),我们将定义一个正则表达式来匹配端点:
var RE_BATTERY = /\/battery\/?/;
回到createServer()
参数,它将是一个提供对请求(和响应)对象的访问的函数,该对象又具有带有所请求URL的字段。 放在一起,我们应该有以下代码:
var server = http.createServer(function (request, response) {
var requestUrl = request.url;
if (RE_BATTERY.test(requestUrl)) {
getBatteryStatus(response, onBatteryInfo, onError);
}
}).listen(PORT);
getBatteryStatus()
是我们稍后将定义的函数。 我们将使用两个response
方法( write()
和end()
将响应发送给调用方的职责委托给此函数。
提供静态内容
除了定义端点之外,我们还需要提供一些静态内容,这些内容将由同一服务器提供。 具有两台服务器的不同设计也是可能的,其中一台用于静态内容,一台用于动态内容。 但是,如果不是有害的话,这可能是不必要的,因为如果我们认为我们将是唯一请求静态内容的客户端,则无需占用更多端口。
即使在这种情况下,HTTP模块也可以解决。 首先,如果客户要求我们的root
,我们将其重定向到我们的主页:
if (requestUrl === '/' || requestUrl === '') {
response.writeHead(301, {
Location: BASE_URL + 'public/demo.html'
});
response.end();
} else if (RE_BATTERY.test(requestUrl)) {
getBatteryStatus(response, onBatteryInfo, onError);
}
然后我们在上面的条件中添加一个`else`分支。 如果请求与我们的任何端点都不匹配,我们的服务器将检查该路径是否存在静态文件,并提供服务,或使用404(未找到)HTTP代码进行响应。
else {
fs.exists(filePath, function (exists) {
if (exists) {
fs.readFile(filePath, function (error, content) {
if (error) {
response.writeHead(500);
response.end();
} else {
response.writeHead(200);
response.end(content, 'utf-8');
}
});
} else {
response.writeHead(404, {'Content-Type': 'text/plain'});
response.write('404 - Resurce Not found');
response.end();
}
});
}
运行操作系统命令
要从Node.js运行操作系统的命令,我们需要另一个名为child_process
模块,该模块还将为我们提供一些实用程序方法。
var child_process = require('child_process');
特别是,我们将使用exec()方法,该方法允许在shell中运行命令并缓冲其输出。
child_process.exec("command", function callback(err, stdout, stderr) {
//....
});
但是,在此操作之前,我们还有几个步骤要遵循:首先,由于我们希望仪表板与多个操作系统配合使用,并且要使电池状态在一个操作系统与另一个操作系统之间不同,因此我们需要一个让服务器表现不同的方式,具体取决于我们当前的操作系统。 不用说,我们需要为我们要支持的所有操作系统确定并测试正确的命令。
识别当前的操作系统
Node.js提供了一种检查基础操作系统的简便方法。 我们需要检查process.platform
,并打开它的值(在命名时要小心一些特质):
function switchConfigForCurrentOS () {
switch(process.platform) {
case 'linux':
//...
break;
case 'darwin': //MAC
//...
break;
case 'win32':
//...
break;
default:
//...
}
}
一旦获得了这些信息,我们就可以专注于在不同平台上检索正确的命令。 除了不同的语法外,返回的字段还将具有不同的命名/格式。 因此,一旦我们检索命令的结果,就必须考虑到这一点。 以下各节介绍了不同操作系统的命令。
操作系统
pmset -g batt | egrep "([0-9]+\%).*" -o
的Linux
upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage"
视窗
wmic Path Win32_Battery
应用模板模式–依赖于操作系统的设计
我们可以检查每个电话正在运行哪个OS,但这似乎很浪费。 底层操作系统是一件事,在我们的服务器生命周期内不太可能更改。 如果我们的服务器进程以某种方式经过封送/拆封,从理论上讲这可能是可行的,但这当然不切实际,也不容易,也不明智。
因此,我们可以在服务器启动时检查当前的操作系统,然后选择最合适的命令并根据其解析功能。
尽管某些细节有所变化,但是在所有操作系统上,处理请求的一般工作流程都是相同的:
- 我们调用
child_process.exec
来运行命令; - 我们检查命令是否成功完成,否则我们将处理错误;
- 假设成功,我们处理命令的输出,提取所需的信息;
- 我们创建一个响应并将其发送回客户端。
这是四本书所描述的Template method design pattern
的完美案例。
由于JavaScript并不是真正面向类的,因此我们实现了模式的一种变体,其中的详细信息(而不是子类的详细信息)被推迟到将被“覆盖”(通过赋值)的函数,具体取决于当前的OS。
function getBatteryStatus(response, onSuccess, onError) {
child_process.exec(CONFIG.command, function execBatteryCommand(err, stdout, stderr) {
var battery;
if (err) {
console.log('child_process failed with error code: ' + err.code);
onError(response, BATTERY_ERROR_MESSAGE);
} else {
try {
battery = CONFIG.processFunction(stdout);
onSuccess(response, JSON.stringify(battery));
} catch (e) {
console.log(e);
onError(response, BATTERY_ERROR_MESSAGE);
}
}
});
}
指令
现在,我们可以将已经找到的关于命令的内容插入到switchConfigForCurrentOS()
函数中。 如上所述,我们需要根据当前OS覆盖命令运行和后处理功能。
function switchConfigForCurrentOS() {
switch (process.platform) {
case 'linux':
return {
command: 'upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage"',
processFunction: processBatteryStdoutForLinux
};
case 'darwin':
//MAC
return {
command: 'pmset -g batt | egrep "([0-9]+\%).*" -o',
processFunction: processBatteryStdoutForMac
};
case 'win32':
return {
command: 'WMIC Path Win32_Battery',
processFunction: processBatteryStdoutForWindows
};
default:
return {
command: '',
processFunction: function () {}
};
}
}
处理Bash输出
我们的策略是为每个OS提供不同版本的后处理方法。 无论平台是什么,我们都希望有一个一致的输出-正如引言中提到的数据API-将相同的信息映射到相同的字段。 为了完成此任务,我们基本上为每个OS定义了输出字段和从数据中检索到的相应字段的名称之间的不同映射。
另一种选择是向客户端发送额外的"OS"
参数,但是我认为引入了耦合。 此外,将服务器(属于服务器)和客户端之间的逻辑分开可能比任何可能的简化或性能提升都要大。
function processLineForLinux(battery, line) {
var key;
var val;
line = line.trim();
if (line.length > 0) {
line = line.split(':');
if (line.length === 2) {
line = line.map(trimParam);
key = line[0];
val = line[1];
battery[key] = val;
}
}
return battery;
}
function mapKeysForLinux(battery) {
var mappedBattery = {};
mappedBattery.percentage = battery.percentage;
mappedBattery.state = battery.state;
mappedBattery.timeToEmpty = battery['time to empty'];
return mappedBattery;
}
function mapKeysForMac(battery) {
var mappedBattery = {};
mappedBattery.percentage = battery[0];
mappedBattery.state = battery[1];
mappedBattery.timeToEmpty = battery[2];
return mappedBattery;
}
function processBatteryStdoutForLinux(stdout) {
var battery = {},
processLine = processLineForLinux.bind(null, battery);
stdout.split('\n').forEach(processLine);
return mapKeysForLinux(battery);
}
function processBatteryStdoutForMac(stdout) {
var battery = stdout.split(';').map(trimParam);
return mapKeysForMac(battery);
}
Windows的处理功能稍微复杂一些,为简单起见,在本文中将其省略。
放在一起
在这一点上,我们只需要做一些接线,用JSON编码我们的数据,以及一些我们仍然需要声明的常量。 您可以查看GitHub上服务器的最终代码 。
结论
在这个迷你系列的第一部分中,我们讨论了我们正在构建的服务的详细信息以及您将学到的知识。 然后,我们介绍了为什么需要服务器以及为什么选择创建RESTful服务。 在讨论如何开发服务器时,我借此机会讨论了如何识别当前操作系统以及如何使用Node.js在其上运行命令。
在本系列的第二部分(也是最后一部分)中,您将发现如何构建客户端部分,以一种很好的方式将信息呈现给用户。
From: https://www.sitepoint.com/creating-a-battery-viz-using-js-getting-started-and-server/