Skip to content

Latest commit

 

History

History
523 lines (434 loc) · 21.1 KB

File metadata and controls

523 lines (434 loc) · 21.1 KB

七、发布内容

第六章增加好友能力是关于增加好友能力的内容。 在社交网络中与其他用户联系的能力是很重要的。 然而,更重要的是提供一个接口来生成内容。 在本章中,我们将实现内容创建背后的逻辑。 我们将涵盖以下议题:

  • 发送和存储文本
  • 显示用户的提要
  • 上传文件

发布和存储文本

在前面的章节中,我们有一个特性需要在应用程序的的前端和后端进行修改。 我们将需要一个接受用户文本的 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', '')清除文本字段,并使用本地errorsuccess变量来表示请求的状态。

在数据库中存储内容

到目前为止,我们有一个 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 请求。 以以下方式执行GETPOST请求变得很容易:

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类型指定inputDOM 元素。 其余的由浏览器完成。

为了提供上传文件的能力,我们需要添加用于文件选择的 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文件中,应用程序流被分为GETPOST请求。 我们将更新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包含一个更适合用户图像的位置。 传递给formidableparse函数的回调接收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 来实现内容创建和交付。 对前端也做了一些更改。

在下一章中,我们将继续添加新特性。 我们将使创建品牌页面和事件成为可能。