在本文中,您将学习如何使用IBM Cloud为组织的前台编写Node.js应用程序,该应用程序需要登录和注销访客。 在此过程中,您将学习如何使用Node.js,Express HTTP服务器库和Cloudant数据库。 您将在高度可用的IBM Cloud中学习如何执行此操作。 这是对IBM Cloud平台上的Node.js编程的基本介绍。
构建应用程序所需的条件
- 一个免费的IBM Cloud帐户。
- HTML的基本知识。
- JavaScript的基础知识。
入门
首先,我们创建应用程序。 然后我们创建开发环境。
入门
首先,我们创建应用程序。 然后我们创建开发环境。
创建应用程序
- 登录到IBM Cloud Console 。 如果您没有IBM帐户,请在此处创建一个 。
- 展开汉堡包图标,然后选择Cloud Foundry Apps 。
- 单击创建资源 。
- 选择类别Cloud Foundry Apps 。 在该类别中,单击Node.js的 SDK。
- 单击SDK forNode.js 。
- 输入以下参数:
应用名称 选择一个未使用的值 主机名 接受默认 域 mybluemix.net 地区/位置 选择离您最近的那个 组织空间 保留默认值 - 点击创建 。
- 单击左侧边栏的概述 。
- 点击访问应用程序URL 。
- 您可能必须重新加载,直到创建并启动该应用程序。
- 图1显示了初始应用程序。 看到它之后,不要关闭该选项卡。 稍后您将需要它。
注意:这将是您的初始应用程序。 如果您转到示例应用程序的URL,您将获得一个不同的URL,这是我们在本文后面构建的用户界面。
图1.初始应用
创建开发环境
IBM Cloud上的Cloud Foundry允许多个开发环境。 在本文中,我们走了简单的道路,并使用了Web开发环境。
- 返回到该应用程序的IBM Console页面。 如果您已注销,请重新登录并在仪表板上单击应用程序的名称。
- 向下滚动至连续投放,然后点击启用 。
- 要接受默认值,请单击创建 。
- 创建工具链后,单击Eclipse Orion Web IDE 。
- 单击创建新的启动配置> + 。
- 单击“ 保存”以创建默认的启动配置。 它具有我们需要的一切。
- 单击访问者日志>app.js 。
- 用此处的代码替换文件的内容。
- 单击播放图标以应用更改。
- 等到再次看到绿色圆圈。 这意味着应用程序已使用新代码重新启动。
- 返回到应用程序选项卡,并在URL的末尾添加/ hello。 看到您收到Hello世界消息。
为了理解该程序,我们逐行遍历源代码( 可在此处找到) 。
第一行被JavaScript解释器忽略为注释。 之所以在这里,是因为IBM Console编辑器包括Eslint实用程序,用于识别代码中的潜在错误。 该指令告诉该实用程序将JavaScript视为Node.js(服务器端JavaScript)代码。 您可以在此处阅读有关Eslint实用程序的更多信息 。
/*Eslint-env node*/
这是用JavaScript编写注释的另一种方法。 双斜杠(//)之后的其余行将被忽略。
//------------------------------------------------------------------------------
// node.js starter application for Bluemix
//------------------------------------------------------------------------------
Node.js是运行时环境。 Web服务器软件包为Express。 您可以在此处了解更多信息 。 在Node.js中使用软件包的方式是将require
函数与软件包名称一起使用(假定该软件包在package.json文件中声明)。 require
函数通常返回用于使用模块的函数。
// This application uses express as its web server
// for more info, see: http://expressjs.com
var express = require('express');
cfenv
模块提供Cloudy Foundry信息。 以后使用它来获取Web服务器需要侦听的端口号以及它正在提供的URL。
// cfenv provides access to your Cloud Foundry environment
// for more info, see: https://www.npmjs.com/package/cfenv
var cfenv = require('cfenv');
下一步是创建HTTP服务器。
// create a new express server
var app = express();
这就是我们告诉Express从目录提供静态文件的方式。 如果在编辑器中打开公共目录,您将看到index.html文件,它是默认应用程序的一部分(图1中所示)。
// serve the files out of ./public as our main files
app.use(express.static(__dirname + '/public'));
app.get
调用指定如何处理对特定路径的请求。 路径是第一个参数。 第二个参数是在请求路径时调用的函数。 该函数有两个参数:一个包含HTTP请求的请求对象,以及一个用于发送回响应的响应对象。 JavaScript为函数文字提供了两种语法选项(一个匿名函数本身可以是一个函数参数)。 我们在这里使用的较旧的是function(<parameters>) {body}
。
回调函数仅使用res.send
函数发送常量字符串,即传统的hello world。
/* @callback */
注释用于Eslint。 通常,Eslint会报告一个函数有一个未使用的参数,建议您删除它。 但是,回调函数的参数由调用代码(此处为Express)设置。 在这种情况下,某些路径回调需要两个参数。
app.get("/hello", /* @callback */ function(req, res) {
res.send("Hello, world");
});
此步骤将获得Cloud Foundry环境。 这就是IBM Cloud基础架构与应用程序通信的方式。 您可以在此处了解更多信息 。
// get the app environment from Cloud Foundry
var appEnv = cfenv.getAppEnv();
// start server on the specified port and binding host
app.listen(appEnv.port, '0.0.0.0', function() {
// print a message when the server starts listening
console.log("server starting on " + appEnv.url);
});
访客日志
该应用程序的主要数据结构是访问者日志。 您可以在此处查看带有声明和操作代码的源代码 。 替换当前的app.js,然后单击播放按钮以重新启动应用程序。 您可以在https:// <您的应用程序> / test / visitors中查看当前的访问者日志。 它采用JSON格式 ,因此如果使用JSON格式化程序(例如this) ,可能会更清楚。 要查看我的应用程序实例的访问者日志,请单击此处 。
访问者日志是一个键为访问者名称的关联数组 (也称为哈希表或对象)。 每个访问者名称的值本身就是一个关联数组,可以具有两个属性:
- 到达 –访客最后一次到达办公室。 如果访客当前不在办公室,则此属性不存在。
- 历史记录 –访问者的历史记录,它是与前次访问的开始和结束时间关联的数组。 第一次访问办公室期间该属性不存在。
默认访问者日志
有一个默认的访客日志,其中包含一些用于调试的示例数据。 如图2所示。
图2.默认的访问者日志
您可以在GitHub的第23-48行中看到此默认访问者日志的定义。 它大多数是标准JavaScript对象定义,但是日期不是固定的,而是在应用程序启动之前的固定时间。 Date.now()
是从现在到固定时间点(格林尼治标准时间1970年1月1 日午夜)之间的毫秒数。 因此,Bill Hamm的到达时间是在应用程序启动时执行此代码之前的两个小时。 您可以在此处阅读有关Date对象的更多信息 。
"Bill Hamm": {
arrived: new Date(Date.now() - 1000*3600*2)
},
阅读访客日志
在显示访客日志时,我们可能想要显示所有日志,或者我们只想显示当前的访客,即办公室中当前的访客。 同样,在没有他们的全部信息的情况下,获取所有访客,当前办公室中的所有访客或当前不是所有访客的名称也很有用。 这些是实现这些要求的功能:
要获取访问者名称列表,我们所需要做的就是获取visitors对象的键。 实现此功能的函数是Object.keys
。 请注意,这里的函数语法是新的语法, (parameters) => {expressions;}
。 在这种情况下,没有参数。
var visitorNames = () => {
return Object.keys(visitors);
};
要获取子集(无论是在办公室的人还是不在办公室的人),请使用filter
功能。 此函数是一个数组方法(因此您可以在任何列表上调用它,例如visitorNames
返回的visitorNames
)。 它得到一个参数,一个函数。 此函数从列表中获取一个项目作为参数,并返回该项目是否应为已过滤列表的成员。
功能定义被简化。 当函数文字是单个表达式时,无需将其括在大括号( {}
)中并具有return
语句。 您可以只输入表达式。
var currentVisitorNames = () => {
return visitorNames().filter((name) => visitors[name].arrived !== undefined);
};
以名称作为关键字来获取当前访问者列表及其信息会更加复杂。 第一步在currentVisitorList
。 此功能使用map
功能检索当前访问者列表中每个用户的信息。 该函数接收函数作为参数,类似于filter
。 但是,不是决定某个值是否将在输出中,而是使用map
的函数来转换该值。 这些转换后的值将以与原始值相同的顺序转换为列表。 您可以在此处阅读有关此功能的信息 。
在这种情况下,对于每个名称,我们都会获得一个包含用户信息的结构。 例如,对于默认数据,我们可能会得到如图3所示的内容。
var currentVisitorNames = () => {
return visitorNames().filter((name) => visitors[name].arrived !== undefined);
};
图3.当前访问者列表
此列表包含我们所需的信息,但是每个用户都在列表中的单独对象中。 使用我们在visitors
变量中使用的相同格式会更有意义,一个对象的每个用户都是一个单独的属性。 为此,我们使用reduce
将列表变成单个对象。 如果收到的列表中有一项,则返回该项目。 如果有两个项目,则对它们运行参数函数并返回结果。 如果还有更多,它将在其中两个参数上运行参数函数,然后在结果和另一个结果上运行参数函数,直到仅剩一个值为止。 您可以在此处阅读有关该功能的更多信息 。
var currentVisitors = () => {
return currentVisitorList().reduce((a, b) => {
要创建组合对象,我们首先需要新对象的密钥,即列表中的密钥,该密钥仅对单个用户具有单个属性。
var bKey = Object.keys(b)[0];
我们将此值添加到原始对象中,在该对象中我们累积了所有用户,然后将其返回。
a[bKey] = b[bKey];
return a;
});
};
修改访客日志
访客日志有两种可能的修改。 用户可以在进入办公室时登录,或者在离开办公室时注销。
对于这些函数,我们具有一个get函数和一个set函数,以与visitors
变量进行交互。 这些功能的目的是使将来轻松修改应用程序以使用其他数据存储(例如数据库)。
var getVisitor = (name) => visitors[name];
var setVisitor = (name, values) => visitors[name] = values;
注销功能相对简单。 检查用户是否具有arrived
属性。 如果该用户没有该用户,则该用户当前不在办公室,因此无法注销。 报告错误。 如果arrived
属性确实存在,请将其删除,并将刚刚结束的访问添加到历史记录中。
var logOut = (name) => {
var oldRecord = getVisitor(name);
如果在反斜线( `
)中包含字符串,则表示它是模板文字 。 它可以运行多行,并且可以使用语法${< expression >}
代替表达式的值,例如name变量。 如果根本没有用户,这是我们返回的错误:
if (oldRecord === undefined)
return `Error, ${name} is unknown`;
如果用户存在,这是我们返回的错误,但目前未登录。
if (oldRecord.arrived === undefined)
return `Error, ${name} is not logged in`;
旧记录中唯一仍然相关的属性是history
。 我们需要保留它并添加新的价值。 但是,如果这是第一次访问,可能还没有历史记录。 在这种情况下,历史记录为空。
var history = oldRecord.history;
// If this is the first visit
if (history === undefined)
history = [];
unshift
函数在数组的开头添加一个新值。 这是我们想要的行为,因为最近的访问更有可能是相关的,因此最好将数组按相反的时间顺序排序。 您可以在此处阅读有关此功能的更多信息 。
history.unshift({
arrived: oldRecord.arrived,
left: new Date(Date.now())
});
当前未登录的访问者的唯一属性是history
。
setVisitor(name, {history: history});
return `OK, ${name} is logged out now`;
};
登录功能使用现有历史记录创建新记录,并使用当前时间创建arrived
属性。
var logIn = (name) => {
var oldRecord = getVisitor(name);
var history;
如果您有多个条件,并且只希望处理第一个为真的条件,则可以使用以下语法:
if <condition A> <action A> else if <condition B> <action B> else if <condition C> <action C> … else <default action>
由于else if
语句,一旦条件被评估为真,将对其执行操作,并跳过其余条件。
在这里,我们需要检查是否存在现有条目,然后再检查它是否具有arrived
属性,因为检查未定义值的属性是错误的。
// First time we see this person
if (oldRecord === undefined)
history = []; // No history
// Already logged in
else if (oldRecord.arrived !== undefined)
return `Error, ${name} is already logged in`;
// Not logged in, already exists
else history = oldRecord.history;
setVisitor(name, {
arrived: new Date(Date.now()),
history: history
});
return `OK, ${name} is logged in now`;
};
测验
上面的代码看起来应该可以工作,但是最好在继续之前对其进行测试。 出于测试目的,/ test下的各种路径返回上述函数的结果。 这样做依赖于testFunctions
,它是一个具有路径及其关联功能的表。 该表中的函数不接收任何参数,并返回信息以发送给用户。 为了能够测试确实需要参数的功能,例如logIn
,将它们包装在确实提供必要参数的定义中的表中。 这样,使用表的代码无需指定任何参数值。
var testFunctions = [
前几行只是不接受参数的函数的函数名称。 无需包装它们。
{path: "visitorNames", func: visitorNames},
{path: "currentVisitorNames", func: currentVisitorNames},
{path: "nonCurrentVisitorNames", func: nonCurrentVisitorNames},
{path: "currentVisitorList", func: currentVisitorList},
{path: "currentVisitors", func: currentVisitors},
/ test / visitors路径实际上并不显示函数的输出,而是变量。 但是,使用该表的代码需要一个函数-因此我们将其包装在一个函数中。
{path: "visitors", func: () => visitors},
logIn
和logOut
函数确实获得一个参数,即用户名。 要运行此处需要的简单测试,我们使用固定的用户名。
{path: "logIn", func: () => logIn("Avimelech ben-Gideon")},
{path: "logOut", func: () => logOut("Avimelech ben-Gideon")}
];
这是注册处理程序的代码。 它在路径前加上/test/
并创建一个函数,该函数从表中调用该函数,然后将响应发送到浏览器。
testFunctions.map((item) =>
app.get(
`/test/${item.path}`,
/* @callback */ function(req, res) {
res.send(item.func());
}
)
);
要实际测试该应用程序,请首先转到/ test / logOut以注销不存在的Avimelech ben-Gideon帐户,然后查看错误代码。 然后两次进入/ test / logIn 。 第一次尝试应该成功,而第二次失败。 然后两次进入/ test / logOut ,以查看第一次尝试成功,而第二次尝试失败。 执行完这些步骤之后,也许还要登录和注销几次,请转到/ test / visitors以查看访客日志的JSON数据。 最简单的方法是转到一个接受URL并为其提供访问者日志URL 的JSON格式化程序 。 最后,查看其他功能URL,例如/ test / currentVisitorNames ,以查看显示正确的信息。
请注意,如果您在示例应用程序上查看这些路径,那么本文其他读者可能会提供过多的信息。
用户界面
将app.js中的代码替换为在此处找到的文件 。 然后转到您的应用程序( 或示例应用程序 )上的/ visitors以查看完整的访问者列表。 转到/ currentVisitors以查看当前列表。 使用/ login登录用户,然后使用/ logout注销用户 。 要查看整个用户界面,请转到/或/index.html 。
第一个区别是该指令发送给Eslint,以忽略未使用的参数。 这是因为/* callback */
指令(仅对此函数调用忽略未使用的参数)。
/*eslint-disable no-unused-params */
知道访客在办公室呆了多长时间很有用,但是很难解释一个数字(例如5000秒)。 此函数将以毫秒为单位的时差转换为可读的字符串。 输入的时间是毫秒,因为这是大多数与时间相关JavaScript函数的标准。
// Given a time difference in miliseconds, return a string
// with the approximate value
var tdiffToString = (msec) => {
var sec = msec/1000;
如果秒数小于60,则返回正确标记的秒数。 使用三进制运算符可以为复数添加“ s”,也可以不添加。 该运算符采用三个参数。 如果第一个参数(少于2秒)为true,则返回第二个参数(空字符串)。 如果第一个参数为假,这意味着我们需要使用复数,则运算符将返回第三个参数,即复数“ s”。
if (sec < 60)
return sec + " second" + (sec < 2 ? "" : "s");
如果秒数少于60,则该函数已经返回。 如果数字介于60到3600(一个小时)之间,则需要返回时间间隔中的分钟数。 注意使用Math.floor
。 这是必要的,因为我们要避免报告5.23544分钟之类的值。
if (sec < 3600)
return Math.floor(sec/60) + " minute" + (sec < 60*2 ? "" : "s");
该功能的其余部分(持续数小时和数天)的运行方式相同。
…
};
这是产生HTML的第一个函数。 用户界面在HTML表格中显示历史记录(到达,离开和中间时间),并且此函数在该表格中生成行。
// Given a history entry (arrived and left times), create a table row with
// that information
var histEntryToRow = (entry) => {
return `<tr>
<td>${entry.arrived}</td>
<td>${entry.left}</td>
<td>${tdiffToString(entry.left-entry.arrived)}</td>
</tr>`;
};
这个函数产生一个整个表(如果有什么要产生,否则它只返回一个空字符串)。
// Given a history, create a table with it
var histToTable = (history) => {
if (history === undefined)
return "";
if (history.length === 0)
return "";
人们并不总是希望看到历史。 通过将其包含在details
标签中,我们可以隐藏历史记录,直到用户决定打开它为止。 style
属性允许我们指定背景颜色,以使历史记录表与其余访客信息(也以表格形式显示)区分开。
return `<details>
<table border style="background-color: yellow">
<tr>
<th>
Arrived
</th>
<th>
Left
</th>
<th>
Time here
</th>
</tr>
${history.map(histEntryToRow).reduce((a, b) => a+b)}
</table>
</details>`;
};
接下来的两个函数userToRow
和usersToTable
与我们创建历史表的方式非常相似。 没有必要去解决它们。 接下来, visitorsHTML
和currentVisitorsHTML
添加标题,并使用适当的用户名列表创建表。 同样,这些功能是如此相似,足以看到其中之一。
var visitorsHTML = () => {
return `
<h2>Current Visitor List</h2>
${usersToTable(visitorNames())}
`;
};
此功能创建一个登录表单。 它使用带有GET方法的HTML表单 。 这意味着将在URL中对参数(在这种情况下,只有一个user
)进行编码。 login
属性提供了处理程序的路径,您将在本文后面看到。
var loginForm = () => {
return `<h2>Log in a visitor</h2>
<form method="get" action="login">
Visitor to log in: <input type="text" name="user">
</form>`;
};
登录表单和注销“表单”之间存在实质性差异。 无法预先知道要登录哪个访客。甚至可能是完全陌生的人。 但是,唯一可以注销的访问者是当前登录的访问者。这使我们可以显示列表并让用户从中进行选择。
var logoutForm = () => {
if (currentVisitorNames().length === 0)
return "No users to log out";
return `
<h2>Log out a visitor</h2>
<ul>
${currentVisitorNames()
我决定不使用输入字段来创建表单,而是决定使用链接。 由于GET表单返回其参数的方式,我们可以这样做并模拟GET表单。 你有通常的URL(HTTP:// <主机名> / <路径>),后跟一个问号,然后<parameter>=<value>
对,由符号(分隔&
)。 该值可能包含在URL中具有特殊含义的字符(例如“&”号),因此会对其进行转义。 这是encodeURI
函数的角色。
.map(name => `<li>
<a href="logout?user=${encodeURI(name)}">${name}</a>
</li>`)
.reduce((a,b) => a + b)}
</ul>`;
};
此功能接收文本,例如上述函数生成HTML,并将其转换为完整的网页。
var embedInHTML = (str) => {
return `<html><body>${str}</body></html>`;
};
这两个调用调用适当的函数来创建标题和表,将其嵌入页面中,然后进行响应。
app.get("/visitors", (req, res) => {
res.send(embedInHTML(visitorsHTML()));
});
app.get("/currentVisitors", (req, res) => {
res.send(embedInHTML(currentVisitorsHTML()));
});
这两个调用是表单处理程序。 使用GET方法提交给表单的参数在req.query
中可用。 如果没有用户,请发送表格。 如果有用户,请为该用户处理命令。
app.get("/login", (req, res) => {
if (req.query.user === undefined)
res.send(embedInHTML(loginForm()));
else
res.send(logIn(req.query.user));
});
app.get("/logout", (req, res) => {
if (req.query.user === undefined)
res.send(embedInHTML(logoutForm()));
else
res.send(logOut(req.query.user));
});
这个app.get
调用显示了应用程序中的所有内容。 路径而不是路径,Express解释为“此处理程序适用于此列表中的所有内容”。
app.get([“ / index.html”,“ /”],(req,res)=> {
app.get(["/index.html", "/"], (req, res) => {
res.send(embedInHTML(`
${loginForm()}
<hr />
${logoutForm()}
<hr />
${currentVisitorsHTML()}
<hr />
${visitorsHTML()}
`));
});
如果我们保留此调用,它将覆盖我们注册的处理程序-因此我将其注释掉。
/*
// serve the files out of ./public as our main files
app.use(express.static(__dirname + '/public'));
*/
数据库
应用程序存在一个“轻微”问题。 每次重新启动它都会丢失访客日志。 为了解决这个问题,我们将访问者日志存储在Cloudant数据库中。 访客名称是关键,而数据(历史记录,到达时间和日期)是值。
创建数据库
请按照以下步骤创建数据库:
- 在IBM Cloud控制台中 ,从汉堡菜单中单击“ 数据和分析” 。
- 单击创建资源,然后选择Cloudant NoSQL DB。
- 将服务
visitor-log
命名为,然后点击创建 。 - 创建服务后,单击左侧栏中的“ 服务凭据 ”。
- 单击新建凭据按钮。
- 将新的凭据
visitor-log
命名为,然后单击添加 。 - 在凭证列表中,点击查看凭证 。 单击复制/复制图标以复制凭据并将其粘贴到文本文件中。
- 在左侧边栏中,点击管理 。 然后,点击启动按钮。
- 在左侧边栏中,单击“数据库”图标。
- 然后,单击右上角的创建数据库 。
- 将数据库命名为
visitor-log
。 - 返回到应用程序开发环境并打开文件package.json 。
- 在依赖项中,为Cloudant软件包(版本1.7.x)添加一行。 完成后,您的文件将看起来像链接中的文件 。
使用数据库
要使用数据库,请用以下代码替换app.js。 它使用几乎相同的数据定义和用户界面,因此看起来不会有所不同。 但是,如果重新启动应用程序,则信息不会丢失。
首先,为您的Cloudant凭证设置一个变量。 显然,您不能在GitHub中使用该值,因为每个用户的凭据都不相同,而且我也没有透露我的信息。
var cloudantCred = {
<<
redacted
>>
};
使用此凭据连接到数据库管理系统,然后指定要使用的数据库:
// Connect to the database
var cloudant = require("cloudant")(cloudantCred.url);
var mydb = cloudant.db.use("visitor-log");
我们可以通过两种方式将信息存储在数据库中。 一种方法是将整个visitors
数据结构放在一个数据库条目中(在Cloudant中称为文档)。 另一种方法是为每个访问者姓名准备一个单独的文档。 在这篇介绍性文章中,我选择使用第一种方法。 显然它的效率较低,但是最多可以使用1 MB 。 这种方法的主要优点是我们可以使用以前开发的应用程序逻辑,而几乎无需更改。 我们可以继续将visitors
数据结构保持为变量。
同一Cloudant数据库可以同时由多个应用程序使用。 为了避免一个应用程序干扰另一个应用程序,它使用修订系统。 每个文档的当前修订版都有一个_rev
值。 更新文档时,您将提供要更新的修订。 如果从您阅读之日起它已更改,则修订版本会有所不同,从而导致更新失败-这要求我们有一个变量来保存当前修订版本。
var dbRev = "";
此功能更新数据库上的访客日志。
var updateVisitors = () => {
要更新的数据始终包含_id
,键和实际值。 它通常还包含_rev
,修订版。 创建新条目时例外。
var updateData = {
"_id": "visitors",
value: visitors
};
if (dbRev !== "")
updateData["_rev"] = dbRev;
准备好更新哈希表后,我们可以使用<database>.insert
函数将最新版本插入数据库。
mydb.insert(updateData,
(err, body) => {
如果插入成功,则响应正文将包括新的修订值。
if (err === undefined)
dbRev = body["rev"];
这些行记录数据库故障。 本文稍后将介绍用于此目的的机制。
else
log += "updateVisitors: " + err + "
";
}
);
};
启动应用程序时,代码中的后几行使用<database>.get
函数读取visitors
。
// Read the visitors data structure.
mydb.get("visitors", (err, body) => {
如果我们收到404错误,则意味着数据库中尚无visitors
数据结构。 在这种情况下,我们创建一个并调用updateVisitors
。 这是在没有修订的情况下调用database.insert
函数的一种情况。
// No visitors yet, create it.
if (err !== null && err.statusCode === 404) {
visitors = {"Test user": {arrived: Date.now().valueOf()}};
updateVisitors();
} else {
如果有值,请使用它,并在updateVisitors
需要它时保留修订。
visitors = body.value;
dbRev = body["_rev"];
}
});
在应用程序启动后,直接更改访问者信息的唯一功能是setVisitor
。 它需要调用updateVisitors
将更改保存在数据库中。
var setVisitor = (name, values) => {
visitors[name] = values;
updateVisitors();
};
时间和日期表示
还有另外一个问题。 由于与Cloudant的通信是基于文本的,因此在写入Cloudant时,Date对象将转换为其文本表示形式,即带有日期,时间和时区的字符串。 然后,当信息被读回时,它们被视为字符串。 如果我们只想显示日期,那还不错,但是我们也希望用户看到每次访问的时间。
为了解决此问题,我们存储的是自纪元开始以来的毫秒数(任意时间点,通常在UNIX派生的系统上设置为1月的午夜),而不是在visitors
数据结构和数据库中存储Date对象。 1970年1月1日,格林尼治标准时间)。 存储毫秒数需要对程序进行一些更改:
首先,我们获取当前日期的每个地方都必须是Date.now().valueOf()
。 调用valueOf()
会将日期对象转换为自纪元开始以来的毫秒数。 例如,在logout
:
history.unshift({
arrived: oldRecord.arrived,
left: Date.now().valueOf()
});
显示日期和时间戳的功能也需要更改。 为了将以毫秒为单位的时间转换为字符串,我们在一个函数histEntryToRow
创建了一个新的Date对象,以毫秒为单位:
// Given a history entry (arrived and left times), create a table row
// with that information
var histEntryToRow = (entry) => {
return `<tr>
<td>${new Date(entry.arrived)}</td>
<td>${new Date(entry.left)}</td>
<td>${tdiffToString(entry.left-entry.arrived)}</td>
</tr>`;
// The Date need to be new, otherwise we are just
// modifying the same object and all dates in
// the history table are the same.
};
出于调试目的记录
在开发此应用程序时,我需要了解为什么有时数据库更新无法正常工作。 为此,我创建了一个日志字符串,并在每次更新失败时将其附加到该字符串。 该技术在Web应用程序中也很有用(确保在拥有生产数据之前将其禁用或要求提供密码)。
need to be new, otherwise we are just
var log = "";
var updateVisitors = () => {
...
mydb.insert(updateData,
(err, body) => {
if (err === undefined)
dbRev = body["rev"];
else
当我们添加到日志时,请添加<br/>
。 该字符串将由浏览器检索,浏览器默认将其解释为HTML。
log += "updateVisitors: " + err + "
";
}
);
};
要访问日志,请转到/ log路径:
app.get("/log", (req, res) => {
res.send(log);
});
结论
现在,您可以在IBM Cloud上编写简单的Node.js Web应用程序,并将数据存储在Cloudant中。 此Web应用程序的主要问题是它在视觉上没有吸引力。 每次更改都需要刷新整个页面,它是纯HTML。
在本系列的下一篇文章中,我将向您展示如何使用Bootstrap主题使Web应用程序看起来更好,以及如何使用AngularJS库使它们更具响应性。