第六章、增加好友能力是关于增加好友能力的内容。 在社交网络中与其他用户联系的能力是很重要的。 然而,更重要的是提供一个接口来生成内容。 在本章中,我们将实现内容创建背后的逻辑。 我们将涵盖以下议题:
- 发送和存储文本
- 显示用户的提要
- 上传文件
在前面的章节中,我们有一个特性需要在应用程序的的前端和后端进行修改。 我们将需要一个接受用户文本的 HTML 表单,一个处理与后端通信的新模型,当然,还有 API 的更改。 让我们从更新主页开始。
我们的有一个显示简单标题的主页。 让我们使用它并添加一个<textarea>
标记来将内容发送给 API。 在本章的后面,我们将使用同一个页面来显示用户的提要。 让我们用以下标记替换孤独的<h1>
标记:
{{#if posting === true}}
<form enctype="multipart/form-data" method="post">
<h3>What is on your mind?</h3>
{{#if error && error != ''}}
<div class="error">{{{error}}}</div>
{{/if}}
{{#if success && success != ''}}
<div class="success">{{{success}}}</div>
{{/if}}
<label for="text">Text</label>
<textarea value="{{text}}"></textarea>
<input type="file" name="file" />
<input type="button" value="Post" on-click="post" />
</form>
{{else}}
<h1>Node.js by example</h1>
{{/if}}
我们仍然在那里有标题,但是只有当posting
变量等于false
时才显示。 在下一节中,我们将更新主页的控制器,我们将使用posting
来保护内容的形式。 在某些情况下,我们不想使<textarea>
可见。
注意,我们有两个块来显示消息。 第一个将是可见的,如果有一个错误在张贴期间和第二个,当一切顺利。 表单的其余部分是所需的用户界面——文本区域、输入文件字段和一个按钮。 这个按钮分派一个我们将在控制器中捕获的 post 事件。
我们肯定需要一个模型来管理与 API 的通信。 让我们创建一个新的models/Content.js
文件,并将以下代码放在那里:
var ajax = require('../lib/Ajax');
var Base = require('./Base');
module.exports = Base.extend({
data: {
url: '/api/content'
},
create: function(content, callback) {
var self = this;
ajax.request({
url: this.get('url'),
method: 'POST',
data: {
text: content.text
},
json: true
})
.done(function(result) {
callback(null, result);
})
.fail(function(xhr) {
callback(JSON.parse(xhr.responseText));
});
}
});
该模块扩展了相同的models/Base.js
类,与我们的系统中的其他模型类似。 因为我们将要发出 HTTP 请求,所以需要lib/Ajax.js
模块。 我们应该熟悉剩下的代码。 通过发送作为参数传递给create
函数的文本,向/api/content
发出POST
请求。
当我们到达文件发布时,模块将被更新。 要创建仅基于文本的记录,这就足够了。
现在我们有了合适的模型和表单,我们准备来调整主页的控制器。 如前所述,posting
变量控制表单的可见性。 它的值默认设置为true
,如果用户没有登录,我们将其更改为false
。 每个 activ .js 组件都可以有一个data
属性。 它表示所有内部变量的初始状态:
// controllers/Home.js
module.exports = Ractive.extend({
template: require('../../tpl/home'),
components: {
navigation: require('../views/Navigation'),
appfooter: require('../views/Footer')
},
data: {
posting: true
}
});
现在,让我们向onrender
处理程序添加一些逻辑。 这是组件的入口点。 我们将从检查当前用户是否登录开始:
onrender: function() {
if(userModel.isLogged()) {
// ...
} else {
this.set('posting', false);
}
}
从第 5 章,管理用户中,我们知道userModel
是一个全局对象,可以用来检查当前用户的状态。 如前所述,如果有未经授权的访问者,我们必须将posting
设置为false
。
下一个逻辑步骤是处理来自表单的内容,并向 API 提交一个请求。 我们将使用新创建的ContentModel
类,如下所示:
var ContentModel = require('../models/Content');
var model = new ContentModel();
var self = this;
this.on('post', function() {
model.create({
text: this.get('text')
}, function(error, result) {
self.set('text', '');
if(error) {
self.set('error', error.error);
} else {
self.set('error', false);
self.set('success', 'The post is saved successfully.<br />What about adding another one?');
}
});
});
一旦用户在表单中按下按钮,我们的组件就会调度一个post
事件。 然后,我们将捕获事件并调用模型的create
方法。 为用户提供适当的响应是很重要的,因此我们使用self.set('text', '')
清除文本字段,并使用本地error
和success
变量来表示请求的状态。
到目前为止,我们有一个 HTML 表单,它向 API 提交一个 HTTP 请求。 在本节中,我们将更新 API,以便在数据库中存储文本内容。 我们模型的端点是/api/content
。 我们将添加一个新的路由,并通过只允许授权用户访问来保护它:
// backend/API.js
.add('api/content', function(req, res) {
var user;
if(req.session && req.session.user) {
user = req.session.user;
} else {
error('You must be logged in in order to use this method.', res);
}
})
我们将创建一个包含访问者会话数据的user
局部变量。 每一个进入数据库的帖子都应该有一个所有者。 因此,在用户的个人资料中设置一个快捷方式是很好的。
同样的/api/content
目录也将用于获取贴子。 同样地,我们将使用req.method
属性来查明将出现何种请求。 如果是GET
,我们需要从数据库中获取贴子并将其发送到浏览器。 如果是POST
,我们必须创建一个新的条目。 下面是将用户文本发送到数据库的代码:
switch(req.method) {
case 'POST':
processPOSTRequest(req, function(data) {
if(!data.text || data.text === '') {
error('Please add some text.', res);
} else {
getDatabaseConnection(function(db) {
getCurrentUser(function(user) {
var collection = db.collection('content');
data.userId = user._id.toString();
data.userName = user.firstName + ' ' + user.lastName;
data.date = new Date();
collection.insert(data, function(err, docs) {
response({
success: 'OK'
}, res);
});
}, req, res);
});
}
});
break;
};
浏览器发送的数据以POST
变量的形式出现。 再次,我们需要processPOSTRequest
的帮助来访问它。 如果没有.text
或者它是空的,API 返回一个错误。 如果一切正常且文本消息可用,我们将继续建立数据库连接。 我们还获取当前用户的整个配置文件。 我们的社交网络中的帖子将与以下附加属性一起保存:
userId
:这个表示记录的创建者。 我们将在提要生成期间使用此属性。userName
:我们不想为我们显示的每一个帖子调用getCurrentUser
。 因此,所有者的名称直接与文本一起存储。 值得一提的是,在某些情况下,这样的调用是必要的。 例如,在更改用户名时将需要调用。date
:我们应该知道数据创建的日期。 它对数据的排序或过滤很有用。
在端,我们调用collection.insert
,它有效地将条目存储在数据库中。
在下一节的中,我们将看到如何检索已创建的内容并将其显示给用户。
现在,每个用户都能够将消息存储在我们的数据库中。 让我们继续在浏览器中显示这些记录。 我们将从向获取邮件的 API 添加逻辑开始。 这将是有趣的,因为您应该得到的消息不仅由一个特定的用户,而且他/她的朋友。 我们使用POST
方法来创建内容。 下面的代码行将处理GET
请求。
首先,我们将通过以下方式获取用户好友的 id:
case 'GET':
getCurrentUser(function(user) {
if(!user.friends) {
user.friends = [];
}
// ...
break;
在前一章中,我们实现了好友功能,并将用户朋友的 id 直接保存在用户的配置文件中。 friends
数组正是我们所需要的,因为我们社交网络中的帖子是通过用户的 id 链接到他们的个人资料的。
下一步是建立到数据库的连接,只查询那些匹配特定 id 的记录,如下所示:
case 'GET':
getCurrentUser(function(user) {
if(!user.friends) {
user.friends = [];
}
getDatabaseConnection(function(db) {
var collection = db.collection('content');
collection.find({
$query: {
userId: { $in: [user._id.toString()].concat(user.friends) }
},
$orderby: {
date: -1
}
}).toArray(function(err, result) {
result.forEach(function(value, index, arr) {
arr[index].id = ObjectId(value.id);
delete arr[index].userId;
});
response({
posts: result
}, res);
});
});
}, req, res);
break;
我们将阅读来自content
集合的记录。 find
方法接受具有$query
和$orderby
属性的对象。 第一个是我们的标准。 在这个特定的例子中,我们想要获得属于friends
数组的所有记录的 id。 为了创建这样的查询,我们需要$in
操作符。 它接受一个数组。 除了用户的朋友的帖子外,我们还需要显示用户的帖子。 因此,我们将创建一个包含条目的数组——即当前用户的 ID——并将其与friends
连接,如下所示:
[user._id.toString()].concat(user.friends)
查询成功后,将删除userId
属性,因为不需要它。 在content
集合中,我们保留了消息的文本和所有者的名称。 最后,记录被发送到posts
属性。
通过在前面的代码中添加的内容,我们的后端返回当前用户及其好友的帖子。 我们所要做的就是更新主页的控制器并使用 API 的方法。 在监听post
事件的代码之后,我们添加以下代码:
var getPosts = function() {
model.fetch(function(err, result) {
if(!err) {
self.set('posts', result.posts);
}
});
};
getPosts();
调用fetch
方法触发对模型端点/api/content
的 API 的GET
请求。 流程被包装在一个函数中,因为当创建一个新帖子时,同样的操作将发生。 正如我们已经知道的,如果model.create
成功,回调将被触发。 我们将在这里添加getPosts()
,以便用户在 feed 中看到他/她的最新帖子:
// frontend/js/controllers/Home.js
model.create(formData, function(error, result) {
self.set('text', '');
if(error) {
self.set('error', error.error);
} else {
self.set('error', false);
self.set('success', 'The post is saved successfully.<br />What about adding another one?');
getPosts();
}
});
结果,getPosts
函数产生的是存储在名为posts
的局部变量中的对象列表。 同样的变量在 ractivejs 模板中也可以访问。 我们需要循环遍历数组中的项并在屏幕上显示信息,如下所示:
// frontend/tpl/home.html
<header>
<navigation></navigation>
</header>
<div class="hero">
{{#if posting === true}}
<form enctype="multipart/form-data" method="post">
...
</form>
{{#each posts:index}}
<div class="content-item">
<h2>{{posts[index].userName}}</h2>
{{posts[index].text}}
</div>
{{/each}}
{{else}}
<h1>Node.js by example</h1>
{{/if}}
</div>
<appfooter />
就在表单之后,我们使用each
操作符来显示帖子的作者和文本。
此时,我们网络中的用户将能够以文本块的形式创建和浏览消息。 在下一节中,我们将扩展到目前为止编写的功能,并使图像和文本一起上传成为可能。
我们正在构建一个单页应用程序。 这类应用程序的特点之一是,所有操作都不需要重新加载页面。 在不改变页面的情况下上传文件总是很棘手。 在过去,我们使用的解决方案包括隐藏 iframe 或小型 Flash 应用程序。 幸运的是,当 HTML5 到来时,它引入了FormData接口。
由于有了XMLHttpRequest
对象,流行的 Ajax 才成为可能。 早在 2005 年,Jesse James Garrett 创造了术语“Ajax”,我们开始使用它在 JavaScript 中发出 HTTP 请求。 以以下方式执行GET
或POST
请求变得很容易:
var http = new XMLHttpRequest();
var url = "/api/content";
var params = "text=message&author=name";
http.open("POST", url, true);
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.setRequestHeader("Content-length", params.length);
http.setRequestHeader("Connection", "close");
http.onreadystatechange = function() {
if(http.readyState == 4 && http.status === 200) {
alert(http.responseText);
}
}
http.send(params);
前面的代码生成了正确的POST
请求,甚至设置了正确的标题。 问题在于参数被表示为字符串。 形成这样的字符串需要额外的努力。 发送文件也很困难。 这很有挑战性。
FormData 接口解决了这个问题。 我们创建一个对象,它是一组键/值对,表示表单字段及其值。 然后,我们将这个对象传递给XMLHTTPRequest
类的send
方法:
var formData = new FormData();
var fileInput = document.querySelector('input[type="file"]');
var url = '/api/content';
formData.append("username", "John Black");
formData.append("id", 123456);
formData.append("userfile", fileInput.files[0]);
var request = new XMLHttpRequest();
request.open("POST", url);
request.send(formData);
我们所要做的就是使用append
方法并使用file
类型指定input
DOM 元素。 其余的由浏览器完成。
为了提供上传文件的能力,我们需要添加用于文件选择的 UI 元素。 下面是在home.html
模板中的表单外观:
<form enctype="multipart/form-data" method="post">
<h3>What is on your mind?</h3>
{{#if error && error != ''}}
<div class="error">{{error}}</div>
{{/if}}
{{#if success && success != ''}}
<div class="success">{{{success}}}</div>
{{/if}}
<label for="text">Text</label>
<textarea value="{{text}}"></textarea>
<input type="file" name="file" />
<input type="button" value="Post" on-click="post" />
</form>
相同的代码,但添加了一个新的input
元素,其类型等于file
。 到目前为止,控制器中发送POST
请求的实现还没有使用 FormData 接口。 让我们改变这个并更新controllers/Home.js
文件:
this.on('post', function() {
var files = this.find('input[type="file"]').files;
var formData = new FormData();
if(files.length > 0) {
var file = files[0];
if(file.type.match('image.*')) {
formData.append('files', file, file.name);
}
}
formData.append('text', this.get('text'));
model.create(formData, function(error, result) {
self.set('text', '');
if(error) {
self.set('error', error.error);
} else {
self.set('error', false);
self.set('success', 'The post is saved successfully.<br />What about adding another one?');
getPosts();
}
});
});
更改了代码。 因此,代码创建一个新的FormData
对象,并使用append
方法来收集新帖子所需的信息。 我们确保附加了用户选择的文件。 默认情况下,HTML 输入只提供一个文件的选择。 但是,我们可以添加multiple
属性,浏览器将允许我们选择多个文件。 值得一提的是,我们过滤选定的文件,只使用图像。
在最新的更改之后,我们模型的create
方法接受FormData
对象,而不是一个普通的 JavaScript 对象。 因此,我们也必须更新模型:
// models/Content.js
create: function(formData, callback) {
var self = this;
ajax.request({
url: this.get('url'),
method: 'POST',
formData: formData,
json: true
})
.done(function(result) {
callback(null, result);
})
.fail(function(xhr) {
callback(JSON.parse(xhr.responseText));
});
}
data
属性被formData
属性替换。 现在我们知道前端将选定的文件发送给 API。 然而,我们没有处理POST
数据的multipart/form-data
类型的代码。 通过POST
请求发送的文件的处理不是那么简单,而processPOSTRequest
在这种情况下不会完成这项工作。
Node.js 有一个很大的社区,有数千个可用的模块。 我们将使用formidable
模块。 它有一个相当简单的 API,它处理包含文件的请求。 在上传文件的过程中,formidable
将文件保存在服务器硬盘的特定位置。 然后,我们接收到资源的路径。 最后,我们必须决定如何处理它。
在backend/API.js
文件中,应用程序流被分为GET
和POST
请求。 我们将更新POST
案例的主要部分。 下面几行包含了formidable
初始化:
case 'POST':
var formidable = require('formidable');
var uploadDir = __dirname + '/../static/uploads/';
var form = new formidable.IncomingForm();
form.multiples = true;
form.parse(req, function(err, data, files) {
// ...
});
break;
如前所述,模块将上传的文件保存在硬盘上的一个临时文件夹中。 变量uploadDir
包含一个更适合用户图像的位置。 传递给formidable
的parse
函数的回调接收data
参数中的正常文本字段,并上传files
中的图像。
为了避免嵌套 JavaScript 回调的长链,我们将在函数定义中提取一些逻辑。 例如,将文件从temporary
文件夹移动到static
文件夹可以通过以下方式执行:
var processFiles = function(userId, callback) {
if(files.files) {
var fileName = userId + '_' + files.files.name;
var filePath = uploadDir + fileName;
fs.rename(files.files.path, filePath, function() {
callback(fileName);
});
} else {
callback();
}
};
我们不想把不同用户的文件混在一起。 因此,我们将使用用户的 ID 并创建他/她自己的文件夹。 还有一些其他的问题我们需要考虑。 例如,我们可以为每个文件创建子文件夹,这样就可以防止覆盖已经上传的资源。 然而,为了使代码尽可能简单,我们将在这里停止。
下面是将 post 保存到数据库的完整代码:
case 'POST':
var uploadDir = __dirname + '/../static/uploads/';
var formidable = require('formidable');
var form = new formidable.IncomingForm();
form.multiples = true;
form.parse(req, function(err, data, files) {
if(!data.text || data.text === '') {
error('Please add some text.', res);
} else {
var processFiles = function(userId, callback) {
if(files.files) {
var fileName = userId + '_' + files.files.name;
var filePath = uploadDir + fileName;
fs.rename(files.files.path, filePath, function(err) {
if(err) throw err;
callback(fileName);
});
} else {
callback();
}
};
var done = function() {
response({
success: 'OK'
}, res);
}
getDatabaseConnection(function(db) {
getCurrentUser(function(user) {
var collection = db.collection('content');
data.userId = user._id.toString();
data.userName = user.firstName + ' ' + user.lastName;
data.date = new Date();
processFiles(user._id, function(file) {
if(file) {
data.file = file;
}
collection.insert(data, done);
});
}, req, res);
});
}
});
break;
我们仍然需要连接到数据库并获取当前用户的配置文件。 这里的不同之处在于,我们将一个新的file
属性附加到 MongoDB 中存储的对象。
最后,我们需要更新主页的模板,让它显示上传的文件:
{{#each posts:index}}
<div class="content-item">
<h2>{{posts[index].userName}}</h2>
{{posts[index].text}}
{{#if posts[index].file}}
<img src="/static/uploads/{{posts[index].file}}" />
{{/if}}
</div>
{{/each}}
现在,each
循环检查是否有带有文章文本的文件。 如果是,它将显示一个显示图像的img
标记。 有了这个新功能,我们的社交网络用户将能够创建包含文本和图片的内容。
在本章中,我们做了一些对我们的应用程序非常重要的事情。 我们通过扩展后端 API 来实现内容创建和交付。 对前端也做了一些更改。
在下一章中,我们将继续添加新特性。 我们将使创建品牌页面和事件成为可能。