NodeJS-6-x-蓝图-全-

NodeJS 6.x 蓝图(全)

原文:zh.annas-archive.org/md5/9B48011577F790A25E05CA5ABA4F9C8B

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

使用 Node.js 构建的 Web 应用程序越来越受到开发人员的欢迎和青睐。如今,随着 Node.js 的不断发展,我们可以看到许多公司都在使用这项技术开发他们的应用程序。其中,Netflix、Paypal 等公司都在生产环境中使用 Node.js。

托管公司也通过在其平台上支持 Node.js 取得了突破。此外,许多构建工具,如任务运行器、生成器和依赖管理器,也使用 Node.js 引擎出现,如 Grunt、Gulp、Bower 等。

本书将向您展示如何使用 Node.js 生态系统中的所有资源从头开始构建和部署 Node.js 应用程序,并探索云服务以进行测试、图像处理和部署。

处理所有这些工具并充分利用它们是一项非常有趣和激动人心的任务。

我们还将介绍 Docker 容器的概念,以及使用不同工具和服务进行持续集成。

在本书中,我们将学习如何充分利用这种开发方法,使用最新和最先进的技术从头到尾构建十个应用程序。

享受吧!

本书涵盖的内容

在整本书中,我们将探索构建 Node.js 应用程序的不同方式,并了解使用 MVC 设计模式构建基本博客页面的各个元素。我们将学习如何处理不同类型的视图模板,如 EJS 和 SWIG,以及使用命令行工具部署和运行应用程序等更复杂的内容。

我们将涵盖 Restful API 架构的基本概念,以及使用 jQuery、React.js 和 Angular.js 进行客户端通信。

尽管一些内容较为高级,但您将准备好理解 Node.js 应用程序的核心概念,以及如何处理不同类型的数据库,如 MongoDB、MySQL,以及 Express 和 Loopback 框架。

第一章 使用 MVC 设计模式构建类似 Twitter 的应用程序,展示了 MVC 模式应用于 Node.js 应用程序的主要概念,使用 Express 框架、mongoose ODM 中间件和 MongoDB 数据库。我们将了解如何使用 Passport 中间件处理用户会话和身份验证。

第二章 使用 MySQL 数据库构建基本网站,是一个真正深入了解使用关系数据库的 Node.js 应用程序。我们将了解如何使用 Sequelize(ORM)中间件与 Mysql 数据库,如何创建数据库关系,以及如何使用迁移文件。

第三章 构建多媒体应用程序,教会您如何处理文件存储和上传多媒体文件,如图像和视频。我们还将看到如何在 MongoDB 上保存文件名,并如何检索文件并在用户界面上显示。然后,我们将学习如何使用 Node.js 流 API 进行写入和读取。

第四章 不要拍照,要创造 - 面向摄影师的应用程序,涵盖了使用 Cloudnary 云服务上传、存储和处理图像的应用程序,并与 MongoDB 进行交互。此外,我们还将看到如何为用户界面实现 Materialize.css 框架,并介绍使用点文件加载配置变量的方法。

第五章 使用 MongoDB 地理空间查询创建门店定位应用程序,解释了使用 MongoDB 进行地理空间数据和地理位置的核心概念,以及支持 GEOJSON 数据格式的最有用的功能之一,即 2dspheres 索引。您将了解如何将 Google Maps API 与 Node.js 应用程序集成。

第六章,使用 Restful API 和 Loopback.io 构建客户反馈应用程序,探讨了 loopback.io 框架来构建 Restful API。我们将了解 Loopback CLI 的基础知识,以便使用命令行创建整个应用程序。您将学习如何处理使用 MongoDB 的模型之间的关系,以及如何在客户端使用 React.js 与 API 进行通信。

第七章,使用 Socket.io 构建实时聊天应用程序,展示了使用 Socket.io 事件构建聊天应用程序的基础知识,使用 Express 和 jQuery 进行用户界面。它涵盖了任务管理器的基本概念,以及如何使用 Gulp 和 livereload 插件。

第八章,使用 Keystone CMS 创建博客,讨论了完全由 Node.js 制作的 CMS,称为 Keystone。这是对 Keystone 应用程序结构的深入探讨,以及如何扩展框架以创建新模型和视图。此外,我们将看到如何自定义和创建新的 Keystone 主题。

第九章,使用 Node.js 和 NPM 构建前端流程,特别有趣,因为我们将使用 loopback.io 框架创建一个 Restful 应用程序,并使用 AngularJS 进行用户界面。此外,我们将使用不同的构建工具使用命令行和 Node Package Manager(NPM)来连接、缩小和优化图像。我们还将看到如何使用 Heroku toolbelt CLI 来创建和部署应用程序。

第十章,使用持续集成和 Docker 创建和部署,探讨了使用 Node.js 应用程序的持续交付开发过程。您将学习如何将工具集成到您的开发环境中,例如 Github,Codeship 和 Heroku,以处理单元测试和自动部署。本章还教您如何设置环境变量以保护您的数据库凭据,以及如何使用 Docker 容器的概念创建完整的应用程序。

您需要为本书准备什么

本书中的所有示例均使用开源解决方案,并可以从每章提供的链接免费下载。

本书的示例使用许多 Node.js 模块和一些 JavaScript 库,例如 jQuery,React.js 和 AngularJS。在撰写本书时,最新版本为 Node.js 5.6 和 6.1。

在第一章,使用 MVC 设计模式构建类似 Twitter 的应用程序,您可以按照逐步指南安装 Node 和 Node Package Manager(NPM)。

您可以使用您喜欢的 HTML 编辑器。

现代浏览器也会非常有帮助。我们使用 Chrome,但请随意使用您喜欢的浏览器。我们推荐以下之一:Safari,Firefox,Chrome,IE 或 Opera,均为最新版本。

本书的受众

您必须具备 JavaScript、HTML 和 CSS 的基本到中级知识,才能跟随本书中的示例,但在某些章节中可能需要更高级的 Web 开发/Restful API 和 Node.js 模块/中间件知识。不用担心;通过示例,我们将详细介绍所有代码,并为您提供许多有趣的链接。

惯例

在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是一些样式的示例,以及它们的含义解释。

文本中的代码词显示如下:

在继续之前,让我们将欢迎消息从:routes/index.js 文件更改为以下突出显示的代码。

代码块设置如下:

/* GET home page. */
router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express from server folder' });
});

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

/* GET home page. */
router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express from server folder' });
});

新术语和重要单词以粗体显示。例如,屏幕上看到的单词,在菜单或对话框中出现的单词会以这样的方式出现在文本中:“点击“下一步”按钮会将您移动到下一个屏幕”。

警告或重要提示会出现在这样的框中。

提示和技巧会出现在这样。

注意

警告或重要提示会出现在这样的框中。

提示

提示和技巧会出现在这样。

第一章:使用 MVC 设计模式构建类似 Twitter 的应用程序

模型 视图 控制器MVC)设计模式在八十年代在软件行业非常流行。这种模式帮助了许多工程师和公司一段时间内构建更好的软件,而且在 Node.js 的兴起和一些 Node 框架如Express.js(有关 Express.js 及其 API 的更多信息,请访问expressjs.com/)时仍然有用。

注意

正如 Express.js 网站所说,它是“快速、不偏见、极简的 Node.js 网络框架”。

Express.js 是最受欢迎的 Node 框架,许多全球公司都采用了它。因此,在我们的第一个应用程序中,让我们看看如何应用 MVC 模式来创建一个仅使用 JavaScript 作为后端的应用程序。

在本章中,我们将涵盖以下主题:

  • 安装 Node 和 Express 框架

  • MVC 设计模式

  • 处理 Yeoman 生成器

  • 如何使用 Express 生成器

  • 如何处理 Express 模板引擎

  • 用户认证

  • 使用 Mongoose Schema 连接 MongoDB

安装 Node.js

首先,我们需要安装最新的 Node.js 版本。在撰写本书时,Node.js 的最新更新版本是v6.3.0。您可以访问 Node.js 网站nodejs.org/en/并选择您的平台。对于本书,我们使用的是 Mac OS X,但示例可以在任何平台上进行跟踪。

要检查 Node 和Node Package Manager (NPM)版本,请打开您的终端/Shell 并输入以下内容:

  • node -v
    
    
  • npm -v
    
    

该书使用 Node 版本6.3.0和 NPM 版本3.10.3

安装 Yeoman

在本书中,我们将使用一些加速开发过程的工具。其中之一称为Yeoman(更多信息可以在yeoman.io/找到),一个强大的 Web 应用程序生成器。

现在让我们安装生成器。打开您的终端/Shell 并输入以下代码:

npm install -g yo

安装 Express 生成器

对于我们的第一个应用程序,让我们使用官方的 Express 生成器。生成器可以帮助我们创建应用程序的初始代码,并且我们可以修改它以适应我们的应用程序。

只需在您的终端或 Shell 中输入以下命令:

npm install -g express

请注意,-g标志表示在您的计算机上全局安装,以便您可以在任何项目中使用它。

Express 是一个强大的 Node.js 微框架;借助它,可以轻松构建 Web 应用程序。

构建基线

现在开始的项目将是一个完全基于服务器端的应用程序。我们不会使用任何界面框架,如 AngularJS,Ember.js 等;让我们只专注于 express 框架。

这个应用程序的目的是利用所有的 express 资源和中间件来创建一个遵循 MVC 设计模式的应用程序。

中间件基本上是由 express 的路由层激活的函数。名称指的是当路由被激活直到其返回(从开始到结束)。中间件就像名称所暗示的那样处于中间位置。重要的是要记住,函数是按照它们被添加的顺序执行的。

在代码示例中,我们将使用包括cookie-parserbody-parser等中间件。

注意

您可以直接从 Packt Publishing 网站上的书页下载本书中使用的代码,也可以直接从 GitHub 上下载本章和其他所有章节:

github.com/newaeonweb/nodejs-6-blueprints

每个应用程序都被赋予了相关章节的名称,所以现在让我们深入到我们的代码中。

首先,在您的计算机上创建一个名为chapter-01的新文件夹。从现在开始,我们将称这个文件夹为根项目文件夹。在我们继续执行启动项目的命令之前,我们将看到一些关于我们在express命令中使用的标志的内容。

我们使用的命令是express --ejs --css sass -git,其中:

  • express是用于创建应用程序的默认命令

  • --ejs表示使用嵌入式 JavaScript 模板引擎,而不是Jade(默认)

  • --css sass表示使用SASS而不是纯CSS(默认)

  • --git:表示向项目添加一个.gitignore文件

由于我正在使用 git 进行版本控制,使用 express 选项向我的应用程序添加一个.gitignore文件将非常有用。但我会在书中跳过所有 git 命令。

要查看express框架提供的所有选项,可以在终端/Shell 中输入以下内容:

express -h

框架为我们提供了启动项目的所有可用命令:

Usage: express [options] [dir]
 Options:
 -h, --help          output usage information
 -V, --version       output the version number
 -e, --ejs           add ejs engine support (defaults to jade)
 --hbs           add handlebars engine support
 -H, --hogan         add hogan.js engine support
 -c, --css <engine>  add stylesheet <engine> support
                   (less|stylus|compass|sass) (defaults to plain css)
 --git           add .gitignore
 -f, --force         force on non-empty directory

现在,打开你的终端/Shell 并输入以下命令:

express --ejs --css sass -git

终端/Shell 中的输出将如下所示:

 create :
 create : ./package.json
 create : ./app.js
 create : ./.gitignore
 create : ./public
 create : ./public/javascripts
 create : ./public/images
 create : ./public/stylesheets
 create : ./public/stylesheets/style.sass
 create : ./routes
 create : ./routes/index.js
 create : ./routes/users.js
 create : ./views
 create : ./views/index.ejs
 create : ./views/error.ejs
 create : ./bin
 create : ./bin/www
 install dependencies:
 $ cd . && npm install
 run the app:
 $ DEBUG=chapter-01:* npm start

正如你在下面的截图中所看到的,生成器非常灵活,只创建了启动项目所需的最小结构:

构建基线

但在继续之前,我们将进行一些更改。

package.json文件添加更改

在根项目文件夹中打开package.json并添加以下突出显示的代码行:

{ 
    "name": "chapter-01", 
    "description": "Build a Twitter Like app using the MVC design pattern", 
    "license": "MIT", 
    "author": { 
        "name": "Fernando Monteiro", 
        "url": "https://github.com/newaeonweb/node-6-blueprints" 
    }, 
    "repository": { 
        "type": "git", 
        "url": "https://github.com/newaeonweb/node-6-blueprints.git" 
    }, 
    "keywords": [ 
        "MVC", 
        "Express Application", 
        "Expressjs" 
    ], 
    "version": "0.0.1", 
    "private": true, 
    "scripts": { 
        "start": "node ./bin/www" 
    }, 
    "dependencies": { 
        "body-parser": "~1.13.2", 
        "cookie-parser": "~1.3.5", 
        "debug": "~2.2.0", 
        "ejs": "~2.3.3", 
        "express": "~4.13.1", 
        "morgan": "~1.6.1", 
        "node-sass-middleware": "0.8.0", 
        "serve-favicon": "~2.3.0" 
    } 
} 

即使这不是一个高优先级的修改,但将这些信息添加到项目中被认为是一个良好的做法。

现在我们准备运行项目;让我们安装在package.json文件中已列出的必要依赖。

在终端/Shell 中,输入以下命令:

npm install

最后,我们准备好了!

运行应用程序

要运行项目并在浏览器中查看应用程序,请在你的终端/Shell 中输入以下命令:

DEBUG=chapter-01:* npm start

你的终端/Shell 中的输出将如下所示:

chapter-01:server Listening on port 3000 +0ms

你可以只运行npm start,但你不会看到之前带有端口名称的输出;在本章的后面,我们会修复它。

现在,只需查看http://localhost:3000。你将看到 express 的欢迎消息。

更改应用程序的结构

让我们对应用程序的目录结构进行一些更改,并准备好遵循模型-视图-控制器设计模式。

我将列出这次重构的必要步骤:

  1. root项目文件夹内:
  • 创建一个名为server的新文件夹
  1. server文件夹内:
  • 创建一个名为config的新文件夹

  • 创建一个名为routes的新文件夹

  • 创建一个名为views的新文件夹。

  1. 此时不要担心config文件夹;我们稍后会插入它的内容。

  2. 现在我们需要将chapter-01/views文件夹中的error.jsindex.js文件移动到chapter-01/server/views文件夹中。

  3. chapter-01/routes文件夹中的index.jsuser.js文件移动到chapter-01/server/routes文件夹中。

  4. 这里只有一个非常简单的更改,但在开发过程中,更好地组织我们应用程序的所有文件将非常有用。

我们仍然需要在主应用程序文件app.js中更改到这个文件夹的路径。打开项目根文件夹中的app.js文件,并更改以下突出显示的行:

... 
var routes = require('./server/routes/index'); 
var users = require('./server/routes/users'); 

var app = express(); 

// view engine setup 
app.set('views', path.join(__dirname, 'server/views')); 
app.set('view engine', 'ejs'); 
... 

在我们继续之前,让我们将routes/index.js文件中的欢迎消息更改为以下突出显示的代码:

/* GET home page. */ 
router.get('/', function(req, res, next) { 
    res.render('index', { title: 'Express from server folder' }); 
}); 

要运行项目并在浏览器中查看应用程序,请按照以下步骤操作:

  1. 在你的终端/Shell 中输入以下命令:
DEBUG=chapter-01:* npm start

  1. 在你的浏览器中打开http://localhost:3000

  2. 在你的浏览器中的输出将如下所示:更改应用程序的结构

应用程序主屏幕

现在我们可以删除以下文件夹和文件:

  • chapter-01/routes

  • index.js

  • user.js

  • chapter-01/views

  • error.js

  • index.js

更改默认行为以启动应用程序

如前所述,我们将更改应用程序的默认初始化过程。为了完成这个任务,我们将编辑app.js文件并添加几行代码:

  1. 打开app.js,并在app.use('/users', users);函数之后添加以下代码:
      // catch 404 and forward to error handler 
      app.use(function(req, res, next) { 
         var err = new Error('Not Found'); 
          err.status = 404; 
          next(err); 
      }); 

这是一个简单的拦截404错误的middleware

  1. 现在在module.exports = app;函数之后添加以下代码:
      app.set('port', process.env.PORT || 3000); 
      var server = app.listen(app.get('port'), function() { 
          console.log('Express server listening on port ' + 
          serer.address().port); 
      }); 

  1. 打开项目根目录下的package.js文件,并更改以下代码:
      ... 
      "scripts": { 
         "start": "node app.js" 
      }, 
      ... 

注意

如果需要,仍然可以使用调试命令:DEBUG=chapter-01:* npm start

  1. package.json文件是 Node.js 应用程序中极其重要的文件。它可以存储项目的各种信息,如依赖关系、项目描述、作者、版本等等。

  2. 此外,还可以设置脚本来轻松地进行缩小、连接、测试、构建和部署应用程序。我们将在第九章中看到如何创建脚本,使用 Node.js 和 NPM 构建前端流程

  3. 让我们测试一下结果;打开你的终端/Shell 并输入以下命令:

 npm start 

  1. 我们将在控制台上看到相同的输出:
 > node app.js 
Express server listening on port 3000!

使用部分文件重构视图文件夹

现在我们将对views文件夹中的目录结构进行重大更改:我们将添加一个重要的嵌入式 JavaScriptEJS)资源,用于在我们的模板中创建可重用的文件。

它们被称为部分文件,并将使用<% = include %>标签包含在我们的应用程序中。

提示

您可以在官方项目页面ejs.co/上找到有关EJS的更多信息。

views文件夹中,我们将创建两个名为partialspages的文件夹:

  1. 此时pages文件夹将如下所示:

  2. 现在让我们将在views文件夹中的文件移动到pages文件夹中。

  3. views文件夹内创建一个pages文件夹。

  4. views文件夹内创建一个partials文件夹。

  • server/

  • pages/

  • index.ejs

  • error.ejs

  • partials/

  1. 现在我们需要创建将包含在所有模板中的文件。请注意,我们只有两个模板:index.jserror.js

  2. 创建一个名为stylesheet.ejs的文件,并添加以下代码:

      <!-- CSS Files --> 
      <link rel='stylesheet' href='https://cdnjs.cloudflare.com/
       ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.min.css'>
      <link rel='stylesheet' href='/stylesheets/style.css' />

提示

我们将使用最新版本的Twitter Bootstrap UI 框架,目前本书编写时的版本是4.0.0-alpha

  1. 我们正在使用内容传送网络CDN)来获取CSSJS文件。

  2. 创建一个名为javascript.ejs的文件,并添加以下代码:

      <!-- JS Scripts -->
      <script src='https://cdnjs.cloudflare.com/ajax/libs
        /jquery/2.2.1/jquery.min.js'></script>
      <script src='https://cdnjs.cloudflare.com/ajax/libs/
       twitter-bootstrap/4.0.0-alpha/js/bootstrap.min.js'></script>
      </body>
      </html>
  1. 然后创建一个名为header.ejs的文件,并添加以下代码:
      <!-- Fixed navbar --> 
      <div class="pos-f-t"> 
          <div class="collapse" id="navbar-header"> 
              <div class="container bg-inverse p-a-1"> 
                 <h3>Collapsed content</h3> 
                  <p>Toggle able via the navbar brand.</p> 
              </div> 
          </div> 
          <nav class="navbar navbar-light navbar-static-top"> 
               <div class="container"> 
                  <button class="navbar-toggler hidden-sm-up" type=
                    "button"data-toggle="collapse" data-target=
                      "#exCollapsingNavbar2"> 
                      Menu 
                  </button> 
                 <div class="collapse navbar-toggleable-xs"
                   id="exCollapsingNavbar2"> 
                      <a class="navbar-brand" href="/">MVC App</a> 
                      <ul class="nav navbar-nav navbar-right"> 
                          <li class="nav-item"> 
                              <a class="nav-link" href="/login">
                                Sign in
                              </a>
                          </li> 
                          <li class="nav-item"> 
                              <a class="nav-link" href="/signup">
                                Sign up
                              </a> 
                           </li> 
                           <li class="nav-item"> 
                              <a class="nav-link" href="/profile">
                                 Profile</a> 
                           </li> 

                          <li class="nav-item"> 
                              <a class="nav-link" href="/comments">
                                Comments</a> 
                          </li> 
                      </ul> 
                  </div> 
              </div> 
          </nav> 
      </div> 
      <!-- Fixed navbar --> 

  1. 创建一个名为footer.ejs的文件,并添加以下代码:
      <footer class="footer"> 
          <div class="container"> 
              <span>&copy 2016\. Node-Express-MVC-App</span> 
          </div> 
      </footer> 

  1. 让我们在app.js文件中调整视图模板的路径;添加以下代码:
      // view engine setup 
      app.set('views', path.join(__dirname, 'server/views/pages')); 
      app.set('view engine', 'ejs'); 

提示

请注意,我们只添加了已经存在的pages文件夹路径。

  1. 现在我们将用以下代码替换pages/index.ejs中的代码:
      <!DOCTYPE html>
      <html>
      <head>
        <title><%= title %></title>
         <% include ../partials/stylesheet %>
      </head> 
      <body>
          <% include ../partials/header %>
          <div class="container">
            <div class="page-header m-t-1">
              <h1><%= title %></h1>
            </div> 
            <p class="lead">Welcome to <%= title %></p>
          </div>
          <% include ../partials/footer %>
          <% include ../partials/javascript %>
       </body>
       </html>
  1. 让我们对pages/error.ejs中的错误视图文件做同样的操作:
      <!DOCTYPE html> 
      <html> 
      <head> 
           <title>Wohp's Error</title> 
           <% include ../partials/stylesheet %> 
      </head> 
      <body> 
           <% include ../partials/header %> 

          <div class="container"> 
              <div class="page-header m-t-1"> 
                  <h1>Sorry: <%= message %></h1> 
                  <h2><%= error.status %></h2> 
                  <pre><%= error.stack %></pre> 
              </div> 

          </div> 
           <% include ../partials/footer %> 
           <% include ../partials/javascript %> 
      </body> 
      </html> 

我们目前在server文件夹中有以下结构:

  • server/

  • pages/

  • index.ejs

  • error.ejs

  • partials/

  • footer.ejs

  • header.ejs

  • javascript.ejs

  • stylesheet.ejs2

为登录、注册和个人资料添加模板

现在我们有了一个坚实的基础,可以继续进行项目。此时,我们将为登录、注册和个人资料界面添加一些模板文件。

这些页面的预期结果将如下截图所示:

为登录、注册和个人资料添加模板

登录界面

为登录、注册和个人资料添加模板

注册界面

为登录、注册和个人资料添加模板

个人资料界面

  1. 现在让我们创建登录模板。在views文件夹中创建一个名为login.ejs的新文件,并放入以下代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
          <title><%= title %></title> 
          <% include ../partials/stylesheet %> 
      </head> 
      <body> 
        <% include ../partials/header %> 

        <div class="container"> 
            <% if (message.length > 0) { %> 
                <div class="alert alert-warning alert-dismissible
                   fade in" role="alert"> 
                  <button type="button" class="close" data-dismiss=
                     "alert" aria-label="Close"> 
                    <span aria-hidden="true">&times;</span> 
                  </button> 
                  <strong>Ohps!</strong> <%= message %>. 
                </div> 
             <% } %> 
            <form class="form-signin" action="/login" method="post"> 
            <h2 class="form-signin-heading">Welcome sign in</h2> 
            <label for="inputEmail" class="sr-only">Email address</label> 
            <input type="email" id="email" name="email" class="form-
              control" placeholder="Email address" required=""> 
            <label for="inputPassword" class="sr-only">Password</label> 
            <input type="password" id="password" name="password"
             class="form-control" placeholder="Password" required=""> 
            <button class="btn btn-lg btn-primary btn-block" 
               type="submit">Sign in</button> 
            <br> 
            <p>Don't have an account? <a href="/signup">Signup</a> 
               ,it's free.</p> 
          </form> 
        </div> 

        <% include ../partials/footer %> 
        <% include ../partials/javascript %> 
      </body> 
      </html> 

  1. routes/index.js中的索引路由后添加登录路由:
      /* GET login page. */ 
      router.get('/login', function(req, res, next) { 
          res.render('login', { title: 'Login Page', message:
           req.flash('loginMessage') }); 
       }); 

注意

在模板中,我们正在使用connect-flash中间件来显示错误消息。稍后,我们将展示如何安装这个组件;现在不用担心。

  1. 让我们将signup模板添加到views/pages文件夹中。

  2. views/pages中创建一个名为signup.ejs的新文件,并添加以下代码:

      <!DOCTYPE html>
      <html>
      <head>
        <title><%= title %></title>
        <% include ../partials/stylesheet %>
      </head>
      <body>
        <% include ../partials/header %>
        <div class="container">
          <% if (message.length > 0) { %>
             <div class="alert alert-warning" role="alert">
               <strong>Warning!</strong> <%= message %>.
             </div>
           <% } %>
           <form class="form-signin" action="/signup" method="post">
             <h2 class="form-signin-heading">Please signup</h2>
             <label for="inputName" class="sr-only">Name address</label>
             <input type="text" id="name" name="name" class="form-control"
              placeholder="Name" required="">
             <label for="inputEmail" class="sr-only">Email address</label>
             <input type="email" id="email" name="email" class=
               "form-control" placeholder="Email address" required="">
             <label for="inputPassword" class="sr-only">Password</label>
             <input type="password" id="password" name="password" 
               class="form-control" placeholder="Password" required="">
             <button class="btn btn-lg btn-primary btn-block" 
               type="submit">Sign in</button> 
              <br> 
              <p>Don't have an account? <a href="/signup">Signup</a>
                  ,it's free.</p>
            </form>
          </div>
          <% include ../partials/footer %>
          <% include ../partials/javascript %>
        </body>
        </html>
  1. 现在我们需要为注册视图添加路由。打开routes/index.js并在登录路由之后添加以下代码:
      /* GET Signup */ 
      router.get('/signup', function(req, res) { 
          res.render('signup', { title: 'Signup Page', 
             message:req.flash('signupMessage') }); 
      }); 

  1. 接下来,我们将在profile页面添加模板和路由到此页面。在view/pages文件夹内创建一个名为profile.ejs的文件,并添加以下代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
          <title><%= title %></title> 
          <% include ../partials/stylesheet %> 
      </head> 
      <body> 
          <% include ../partials/header %> 
          <div class="container"> 
            <h1><%= title %></h1> 
            <div class="datails"> 
              <div class="card text-xs-center"> 
                  <br> 
                <img class="card-img-top" src="img/<%= avatar %>" 
                  alt="Card image cap"> 
                  <div class="card-block"> 
                    <h4 class="card-title">User Details</h4> 
                    <p class="card-text"> 
                        <strong>Name</strong>: <%= user.local.name %><br> 
                        <strong>Email</strong>: <%= user.local.email %> 
                    </p> 
                    <a href="/logout" class="btn btn-default">Logout</a> 
                  </div> 
              </div> 
            </div> 
         </div> 
        <% include ../partials/footer %> 
        <% include ../partials/javascript %> 
      </body> 
      </html> 

  1. 现在我们需要为 profile 视图添加路由;打开routes/index.js并在signup路由之后添加以下代码:
      /* GET Profile page. */ 
      router.get('/profile',  function(req, res, next) {
          res.render('profile', { title: 'Profile Page', user : req.user,
          avatar: gravatar.url(req.user.email ,  {s: '100', r: 'x', d:
           'retro'}, true) });
      }); 

提示

我们正在使用另一个名为gravatar的中间件;稍后我们将展示如何安装它。

安装额外的中间件

正如您在前面的部分中所看到的,我们使用了一些中间件来显示消息和使用 gravatar 显示用户图标。在本节中,我们将看到如何安装一些非常重要的模块用于我们的应用程序。

由于我们为signinsignupprofile页面创建了模板,我们需要存储具有登录和密码的用户。

这些是我们将用于此任务的中间件,每个中间件的定义如下:

组件 描述 更多细节
connect-flash 用户友好的消息 www.npmjs.com/package/connect-flash
connect-mongo 用于连接 MongoDB 的驱动程序 www.npmjs.com/package/connect-mongo
express-session 在数据库中存储用户会话 www.npmjs.com/package/express-session
Gravatar 显示随机用户图片 www.npmjs.com/package/gravatar
Passport 身份验证中间件 www.npmjs.com/package/passport
passport-local 本地用户/密码验证 www.npmjs.com/package/passport-local

打开您的终端/Shell 并键入:

npm install connect-flash connect-mongo express-session gravatar
      passport passport-local -save

注意

正如我们所看到的,我们将使用 MongoDB 来存储用户数据;您可以在www.mongodb.org/找到有关 MongoDB 的更多信息,并在docs.mongodb.org/manual/installation/找到安装过程。我们假设您已经在您的机器上安装了 MongoDB 并且它正在运行。

使用新中间件重构 app.js 文件

此时,我们需要对app.js文件进行重大重构,以包含我们将使用的新中间件。

我们将逐步向您展示如何包含每个中间件,最后我们将看到完整的文件:

  1. 打开app.js并在var app = express()之前添加以下行:
      // ODM With Mongoose 
      var mongoose = require('mongoose'); 
      // Modules to store session 
      var session    = require('express-session'); 
      var MongoStore = require('connect-mongo')(session); 
      // Import Passport and Warning flash modules 
      var passport = require('passport'); 
      var flash = require('connect-flash'); 

这是一个简单的导入过程。

  1. app.set('view engine', 'ejs')之后添加以下行:
      // Database configuration 
      var config = require('./server/config/config.js'); 
      // connect to our database 
      mongoose.connect(config.url); 
      // Check if MongoDB is running 
      mongoose.connection.on('error', function() {
        console.error('MongoDB Connection Error. Make sure MongoDB is
         running.'); 
      }); 

      // Passport configuration 
      require('./server/config/passport')(passport); 

  1. 请注意,我们在第一行使用了一个config.js文件;稍后我们将创建这个文件。

  2. app.use(express.static(path.join(__dirname, 'public')))之后添加以下行:

      // required for passport 
      // secret for session 
      app.use(session({ 
          secret: 'sometextgohere', 
          saveUninitialized: true, 
          resave: true, 
          //store session on MongoDB using express-session +
          connect mongo 
          store: new MongoStore({ 
              url: config.url, 
              collection : 'sessions' 
          }) 
      })); 

      // Init passport authentication 
      app.use(passport.initialize()); 
      // persistent login sessions 
      app.use(passport.session()); 
      // flash messages 
      app.use(flash()); 

添加配置和护照文件

如前所述,让我们创建一个config文件:

  1. server/config内创建一个名为config.js的文件,并将以下代码放入其中:
      // Database URL 
      module.exports = { 
          // Connect with MongoDB on local machine 
          'url' : 'mongodb://localhost/mvc-app' 
      }; 

  1. server/config上创建一个新文件并命名为passport.js。添加以下内容:
      // load passport module 
      var LocalStrategy    = require('passport-local').Strategy; 
      // load up the user model 
      var User = require('../models/users'); 

      module.exports = function(passport) { 
          // passport init setup 
          // serialize the user for the session 
          passport.serializeUser(function(user, done) { 
              done(null, user.id); 
          }); 
          //       deserialize the user 
          passport.deserializeUser(function(id, done) { 
              User.findById(id, function(err, user) { 
                  done(err, user); 
              }); 
          }); 
          // using local strategy 
          passport.use('local-login', new LocalStrategy({ 
              // change default username and password, to email 
              //and password 
              usernameField : 'email', 
              passwordField : 'password', 
              passReqToCallback : true 
          }, 
          function(req, email, password, done) { 
              if (email) 
              // format to lower-case 
              email = email.toLowerCase(); 
              // process asynchronous 
              process.nextTick(function() { 
                  User.findOne({ 'local.email' :  email }, 
                   function(err, user)
                { 
                  // if errors 
                 if (err) 
                   return done(err); 
                 // check errors and bring the messages 
                 if (!user) 
                   return done(null, false, req.flash('loginMessage',
                   'No user found.')); 
                if (!user.validPassword(password)) 
                  return done(null, false, req.flash('loginMessage',
                  'Wohh! Wrong password.')); 
                // everything ok, get user 
                else 
                  return done(null, user); 
                }); 
              }); 
           })); 
          // Signup local strategy 
          passport.use('local-signup', new LocalStrategy({ 
              // change default username and password, to email and 
             //  password 
              usernameField : 'email', 
              passwordField : 'password', 
              passReqToCallback : true 
          }, 
          function(req, email, password, done) { 
              if (email) 
              // format to lower-case 
              email = email.toLowerCase(); 
              // asynchronous 
              process.nextTick(function() { 
                  // if the user is not already logged in: 
                  if (!req.user) { 
                      User.findOne({ 'local.email' :  email },
                       function(err,user) { 
                  // if errors 
                  if (err) 
                    return done(err); 
                  // check email 
                  if (user) { 
                    return done(null, false, req.flash('signupMessage',
                     'Wohh! the email is already taken.')); 
                  }
                  else { 
                    // create the user 
                      var newUser = new User(); 
                      // Get user name from req.body 
                      newUser.local.name = req.body.name; 
                      newUser.local.email = email; 
                      newUser.local.password =
                       newUser.generateHash(password); 
                      // save data 
                     newUser.save(function(err) { 
                   if (err) 
                     throw err; 
                     return done(null, newUser); 
                    }); 
                   } 
                }); 
               } else { 
                 return done(null, req.user); 
               }         }); 
          })); 
      }; 

请注意,在第四行,我们正在导入一个名为models的文件;我们将使用 Mongoose 创建这个文件。

创建一个 models 文件夹并添加一个用户模式

server/内创建一个 models 文件夹,并添加以下代码:

// Import Mongoose and password Encrypt 
var mongoose = require('mongoose'); 
var bcrypt   = require('bcrypt-nodejs'); 

// define the schema for User model 
var userSchema = mongoose.Schema({ 
    // Using local for Local Strategy Passport 
    local: { 
        name: String, 
        email: String, 
        password: String, 
    } 

}); 

// Encrypt Password 
userSchema.methods.generateHash = function(password) { 
    return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 
}; 

// Verify if password is valid 
userSchema.methods.validPassword = function(password) { 
    return bcrypt.compareSync(password, this.local.password); 
}; 

// create the model for users and expose it to our app 
module.exports = mongoose.model('User', userSchema); 

保护路由

到目前为止,我们已经有足够的代码来配置对我们应用程序的安全访问。但是,我们仍然需要添加一些行到登录和注册表单中,以使它们正常工作:

  1. 打开server/routes/index.js并在login GET路由之后添加以下行:
      /* POST login */ 
      router.post('/login', passport.authenticate('local-login', { 
          //Success go to Profile Page / Fail go to login page 
          successRedirect : '/profile', 
          failureRedirect : '/login', 
          failureFlash : true 
      })); 

  1. signup GET路由之后添加这些行:
      /* POST Signup */ 
      router.post('/signup', passport.authenticate('local-signup', { 
          //Success go to Profile Page / Fail go to Signup page 
          successRedirect : '/profile',       
          failureRedirect : '/signup', 
          failureFlash : true 
      })); 

  1. 现在让我们添加一个简单的函数来检查用户是否已登录;在server/routes/index.js的末尾添加以下代码:
      /* check if user is logged in */ 
      function isLoggedIn(req, res, next) { 
          if (req.isAuthenticated()) 
              return next(); 
          res.redirect('/login'); 
      } 

  1. 让我们添加一个简单的路由来检查用户是否已登录,并在isLoggedIn()函数之后添加以下代码:
      /* GET Logout Page */ 
      router.get('/logout', function(req, res) { 
          req.logout(); 
          res.redirect('/'); 
      }); 

  1. 最后的更改是将isloggedin()作为 profile 路由的第二个参数。添加以下突出显示的代码:
      /* GET Profile page. */ 
      router.get('/profile', isLoggedIn, function(req, res, next) { 
          res.render('profile', { title: 'Profile Page', user : req.user,
           avatar: gravatar.url(req.user.email ,  {s: '100', r: 'x',
             d:'retro'}, true) }); 
      }); 

最终的index.js文件将如下所示:

var express = require('express'); 
var router = express.Router(); 
var passport = require('passport'); 
// get gravatar icon from email 
var gravatar = require('gravatar'); 

/* GET home page. */ 
router.get('/', function(req, res, next) { 
    res.render('index', { title: 'Express from server folder' }); 
}); 

/* GET login page. */ 
router.get('/login', function(req, res, next) { 
    res.render('login', { title: 'Login Page', message: req.flash('loginMessage') }); 
}); 
/* POST login */ 
router.post('/login', passport.authenticate('local-login', { 
    //Success go to Profile Page / Fail go to login page 
    successRedirect : '/profile', 
    failureRedirect : '/login', 
    failureFlash : true 
})); 

/* GET Signup */ 
router.get('/signup', function(req, res) { 
    res.render('signup', { title: 'Signup Page', message: req.flash('signupMessage') }); 
}); 
/* POST Signup */ 
router.post('/signup', passport.authenticate('local-signup', { 
    //Success go to Profile Page / Fail go to Signup page 
    successRedirect : '/profile', 
    failureRedirect : '/signup', 
    failureFlash : true 
})); 

/* GET Profile page. */ 
router.get('/profile', isLoggedIn, function(req, res, next) { 
    res.render('profile', { title: 'Profile Page', user : req.user, avatar: gravatar.url(req.user.email ,  {s: '100', r: 'x', d: 'retro'}, true) }); 
}); 

/* check if user is logged in */ 
function isLoggedIn(req, res, next) { 
    if (req.isAuthenticated()) 
        return next(); 
    res.redirect('/login'); 
} 
/* GET Logout Page */ 
router.get('/logout', function(req, res) { 
    req.logout(); 
    res.redirect('/'); 
}); 

module.exports = router; 

我们几乎已经设置好了应用程序的最终,但我们仍然需要创建一个评论页面。

创建控制器文件夹

我们将使用controllers文件夹来创建评论文件的路由和函数,而不是使用routes文件夹,这样我们可以分离路由和控制器函数,从而实现更好的模块化:

  1. 创建一个名为controllers的文件夹。

  2. 创建一个名为comments.js的文件,并添加以下代码:

      // get gravatar icon from email 
      var gravatar = require('gravatar'); 
      // get Comments model 
      var Comments = require('../models/comments'); 

      // List Comments 
      exports.list = function(req, res) { 
         // List all comments and sort by Date 
          Comments.find().sort('-created').populate('user',
            'local.email').exec(function(error, comments) { 
              if (error) { 
                  return res.send(400, { 
                      message: error       
                  }); 
              } 
              // Render result 
              res.render('comments', { 
                  title: 'Comments Page', 
                  comments: comments, 
                  gravatar: gravatar.url(comments.email,
                     {s: '80', r: 'x', d: 'retro'}, true) 
              }); 
          }); 
      }; 
      // Create Comments 
      exports.create = function(req, res) { 
         // create a new instance of the Comments model with request body 
          var comments = new Comments(req.body); 
          // Set current user (id) 
          comments.user = req.user; 
          // save the data received 
          comments.save(function(error) { 
              if (error) { 
                  return res.send(400, { 
                      message: error 
                  }); 
              } 
              // Redirect to comments 
              res.redirect('/comments'); 
          }); 
      }; 
      // Comments authorization middleware 
      exports.hasAuthorization = function(req, res, next) { 
          if (req.isAuthenticated()) 
              return next(); 
          res.redirect('/login'); 
      }; 

  1. 让我们在app.js文件中导入控制器;在var users = require('./server/routes/users')之后添加以下行:
      // Import comments controller
      var comments = require('./server/controllers/comments'); 
  1. 现在在app.use('/users', users)之后添加评论路由:
      // Setup routes for comments 
      app.get('/comments', comments.hasAuthorization, comments.list); 
      app.post('/comments', comments.hasAuthorization, comments.create); 

  1. server/pages下创建一个名为comments.ejs的文件,并添加以下行:
      <!DOCTYPE html> 
      <html> 
      <head> 
          <title><%= title %></title> 
          <% include ../partials/stylesheet %> 
      </head> 
      <body> 
        <% include ../partials/header %> 
        <div class="container"> 
          <div class="row"> 
            <div class="col-lg-6"> 
              <h4 class="text-muted">Comments</h4> 
            </div> 
            <div class="col-lg-6"> 
              <button type="button" class="btn btn-secondary pull-right"
               data-toggle="modal" data-target="#createPost">
                    Create Comment 
                </button> 
            </div> 
            </div> 
            <!-- Modal --> 
            <div class="modal fade" id="createPost" tabindex="-1"
             role="dialog" aria-labelledby="myModalLabel"
              aria-hidden="true"> 
              <div class="modal-dialog" role="document"> 
                <div class="modal-content"> 
                  <form action="/comments" method="post"> 
                    <div class="modal-header"> 
                      <button type="button" class="close" 
                       data-dismiss="modal" aria-label="Close"> 
                         <span aria-hidden="true">&times;</span> 
                       </button> 
                       <h4 class="modal-title" id="myModalLabel">
                        Create Comment</h4> 
                    </div> 

                    <div class="modal-body"> 
                      <fieldset class="form-group"> 
                         <label  for="inputitle">Title</label> 
                         <input type="text" id="inputitle" name="title"
                           class="form-control" placeholder=
                            "Comment Title" required=""> 
                       </fieldset> 
                       <fieldset class="form-group"> 
                         <label  for="inputContent">Content</label> 
                         <textarea id="inputContent" name="content"
                          rows="8" cols="40" class="form-control"
                          placeholder="Comment Description" required="">
                         </textarea> 
                       </fieldset> 

                       </div> 
                        <div class="modal-footer"> 
                          <button type="button" class="btn btn-secondary"
                           data-dismiss="modal">Close</button> 
                          <button type="submit" class="btn btn-primary">
                           Save changes</button> 
                        </div> 
                  </form> 
                </div> 
              </div> 
            </div> 
              <hr> 
            <div class="lead"> 
              <div class="list-group"> 
                <% comments.forEach(function(comments){ %> 
                  <a href="#" class="list-group-item"> 
                    <img src="img/<%= gravatar %>" alt="" style="float: left;
                      margin-right: 10px"> 
               <div class="comments"> 
                <h4 class="list-group-item-heading">
                  <%= comments.title %></h4> 
                 <p class="list-group-item-text">
                   <%= comments.content %></p> 
                 <small class="text-muted">By: 
                    <%= comments.user.local.email %>
                 </small> 
                </div> 
                </a> 

            <% }); %> 
            </div> 
          </div> 
         </div> 
          <% include ../partials/footer %> 
          <% include ../partials/javascript %> 
      </body> 
      </html> 

  1. 请注意,我们使用了 Twitter-bootstrap 的简单 Modal 组件来添加评论,如下截图所示:创建控制器文件夹

创建评论屏幕的模型

  1. 最后一步是为评论创建一个模型;让我们在server/models/下创建一个名为comments.js的文件,并添加以下代码:
      // load the things we need 
      var mongoose = require('mongoose'); 
      var Schema = mongoose.Schema; 

      var commentSchema = mongoose.Schema({ 
          created: { 
              type: Date, 
              default: Date.now 
          }, 
          title: { 
              type: String,       
              default: '', 
              trim: true, 
              required: 'Title cannot be blank' 
          }, 
          content: { 
              type: String, 
              default: '', 
              trim: true 
          }, 
          user: { 
              type: Schema.ObjectId, 
              ref: 'User' 
          } 
      }); 

      module.exports = mongoose.model('Comments', commentSchema); 

运行应用程序并添加评论

现在是时候测试一切是否正常工作了:

  1. 在项目根目录打开终端/Shell,并输入以下命令:
npm start

  1. 在浏览器中检查:http://localhost:3000

  2. 转到http://localhost:3000/signup,创建一个名为John Doe的用户,邮箱为john@doe.com,密码为123456

  3. 转到http://localhost:3000/comments,点击创建评论按钮,并添加以下内容:

      Title: Sample Title 
      Comments: Lorem ipsum dolor sit amet, consectetur adipiscing elit,
       sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
       Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
       nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
       reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
       pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
       culpa qui officia deserunt mollit anim id est laborum. 

  1. 以下截图展示了最终结果:运行应用程序并添加评论

评论屏幕

检查错误消息

现在让我们检查 flash-connect 消息。转到http://localhost:3000/login,尝试以用户身份登录;我们将使用martin@tech.com和密码123

以下截图展示了结果:

检查错误消息

登录屏幕上的错误消息

现在我们尝试使用已注册的用户进行注册。转到http://localhost:3000/signup,并放置以下内容:

name: John Doe 
email: john@doe.com
password: 123456 

以下截图展示了结果:

检查错误消息

注册屏幕上的错误消息

总结

在这一章中,我们讨论了如何使用 Node.js 和 express 框架创建 MVC 应用程序,这是一个完全在服务器端的应用程序,与使用 Rails 或 Django 框架创建的应用程序非常相似。

我们还建立了安全路由和非常健壮的身份验证,包括会话控制、会话 cookie 存储和加密密码。

我们使用 MongoDB 来存储用户和评论的数据。

在下一章中,我们将看到如何在 express 和 Node.js 中使用另一个数据库系统。

第二章:使用 MySQL 数据库构建基本网站

在本章中,我们将介绍使用关系数据库的 Node.js 应用程序的一些基本概念,本例中为 Mysql。

让我们看一下 MongoDB 的对象文档映射器ODM)和sequelize和 Mysql 使用的对象关系映射器ORM)之间的一些区别。为此,我们将创建一个简单的应用程序,并使用我们可用的资源sequelize,这是一个用于创建模型和映射数据库的强大中间件。

我们还将使用另一个名为 Swig 的引擎模板,并演示如何手动添加模板引擎。

在本章中,我们将涵盖:

  • 如何使用 Swig 模板引擎

  • 将默认路由从 express 生成器更改为 MVC 方法

  • 安装 Squelize-CLI

  • 如何使用 Sequelize 模型的 ORM

  • 使用数据库迁移脚本

  • 处理 MySQL 数据库关系

我们正在构建的内容

在本章末尾,我们将创建以下示例应用程序。本章假设您已经在计算机上安装并运行了 Mysql 数据库。

我们正在构建的内容

示例应用程序

创建基线应用程序

第一步是创建另一个目录,因为我将所有章节都放在 git 控制下,我将使用与第一章相同的根文件夹,在 Node.js 中使用 MVC 设计模式构建类似 Twitter 的应用程序

  1. 创建一个名为chapter-02的文件夹。

  2. 在此文件夹中打开您的终端/ shell 并键入 express 命令:

 express --git

请注意,这次我们只使用了--git标志,我们将使用另一个模板引擎,但将手动安装它。

安装 Swig 模板引擎

要做的第一步是将默认的 express 模板引擎更改为Swig,这是一个非常简单、灵活和稳定的模板引擎,还为我们提供了一个非常类似于 AngularJS 的语法,只需使用双大括号{{ variableName }}表示表达式。

提示

有关Swig的更多信息,请访问官方网站:github.com/paularmstrong/swig

  1. 打开package.json文件并用以下代码替换jade行:
 "swig": "¹.4.2",

  1. 在项目文件夹中打开终端/ shell 并键入:
 npm install

  1. 在我们继续之前,让我们对app.js进行一些调整,我们需要添加Swig模块。打开app.js并在var bodyParser = require('body-parser');行之后添加以下代码:
      var swig = require('swig');

  1. 用以下代码替换默认的jade模板引擎行:
      var swig = new swig.Swig(); 
      app.engine('html', swig.renderFile); 
      app.set('view engine', 'html'); 

重构 views 文件夹

与之前一样,让我们将views文件夹更改为以下新结构:

views

  • pages/

  • partials/

  1. views文件夹中删除默认的jade文件。

  2. pages文件夹中创建一个名为layout.html的文件并放入以下代码:

      <!DOCTYPE html> 
      <html> 
      <head> 
      </head> 
      <body> 
          {% block content %} 
          {% endblock %} 
      </body> 
      </html> 

  1. views/pages文件夹中创建一个index.html并放入以下代码:
      {% extends 'layout.html' %} 
      {% block title %}{% endblock %} 
      {% block content %} 
      <h1>{{ title }}</h1> 
          Welcome to {{ title }} 
      {% endblock %} 

  1. views/pages文件夹中创建一个error.html页面并放入以下代码:
      {% extends 'layout.html' %} 
      {% block title %}{% endblock %} 
      {% block content %} 
      <div class="container"> 
          <h1>{{ message }}</h1> 
          <h2>{{ error.status }}</h2> 
          <pre>{{ error.stack }}</pre> 
       </div> 
      {% endblock %} 

  1. 我们需要在app.js上调整views路径,并在var app = express();函数之后用以下代码替换代码:
      // view engine setup 
      app.set('views', path.join(__dirname, 'views/pages'));

此时,我们已经完成了启动 MVC 应用程序的第一步。在上一章中,我们基本上使用了 express 命令创建的原始结构,但在本例中,我们将完全使用 MVC 模式,即 Model,View,Controller。

创建一个 controllers 文件夹

  1. 在根项目文件夹内创建一个名为controllers的文件夹。

  2. controllers文件夹中创建一个index.js并放入以下代码:

      // Index controller 
      exports.show = function(req, res) { 
      // Show index content 
          res.render('index', { 
              title: 'Express' 
          }); 
      }; 

  1. 编辑app.js文件,并用以下代码替换原始的index路由app.use('/', routes);
      app.get('/', index.show); 

  1. 将控制器路径添加到app.js文件中var swig = require('swig');声明之后,用以下代码替换原始代码:
      // Inject index controller 
      var index = require('./controllers/index'); 

  1. 现在是时候检查一切是否如预期般进行了:我们将运行应用程序并检查结果。在您的终端/ shell 中键入以下命令:
 npm start

检查以下网址:http://localhost:3000,您将看到 express 框架的欢迎消息。

删除默认路由文件夹

让我们删除默认的routes文件夹:

  1. 删除routes文件夹及其内容。

  2. app.js中删除user route,在索引控制器行之后。

为头部和页脚添加部分文件

现在让我们添加头部和页脚文件:

  1. views/partials文件夹中创建一个名为head.html的新文件,并放入以下代码:
      <meta charset="utf-8"> 
      <title>{{ title }}</title> 
      <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs
       /twitter-bootstrap/4.0.0-alpha.2/css/bootstrap.min.css'> 
      <link rel="stylesheet" href="/stylesheets/style.css"> 

  1. views/partials文件夹中创建一个名为footer.html的文件,并放入以下代码:
      <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1
       /jquery.min.js'></script> 
      <script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap
       /4.0.0-alpha.2/js/bootstrap.min.js'></script> 

  1. 现在,是时候使用include标签将部分文件添加到layout.html页面了。打开layout.html并添加以下代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
          {% include "../partials/head.html" %} 
      </head> 
      <body> 
           {% block content %} 
           {% endblock %} 

          {% include "../partials/footer.html" %} 
      </body> 
      </html> 

最后,我们准备继续我们的项目。这次,我们的目录结构将如下截图所示:

为头部和页脚添加部分文件

文件结构

安装和配置 Sequelize-cli

Sequelize-cli是一个非常有用的命令行界面,用于创建模型、配置和迁移文件到数据库。它与 Sequelize 中间件集成,并与许多关系数据库(如 PostgreSQL、MySQL、MSSQL、Sqlite)一起运行。

提示

您可以在以下网址找到有关 Sequelize 中间件实现的更多信息:docs.sequelizejs.com/en/latest/,以及Sequelize-Cli的完整文档:github.com/sequelize/cli

  1. 打开终端/Shell 并键入:
 npm install -g sequelize-cli

  1. 使用以下命令安装sequelize
 npm install sequelize -save

提示

记住我们总是使用-save标志将模块添加到我们的package.json文件中。

  1. 在根文件夹上创建一个名为.sequelizerc的文件,并放入以下代码:
      var path = require('path'); 
      module.exports = { 
        'config': path.resolve('./config', 'config.json'), 
        'migrations-path': path.resolve('./config', 'migrations'), 
        'models-path': path.resolve('./', 'models'), 
        'seeders-path': path.resolve('./config', 'seeders') 
      } 

  1. 在终端/Shell 上,键入以下命令:
sequelize init

  1. init命令之后,终端将显示以下输出消息:
 Sequelize [Node: 6.3.0, CLI: 2.3.1, ORM: 3.19.3] 

      Using gulpfile /usr/local/lib/node_modules/sequelize
      -cli/lib/gulpfile.js 
      Starting 'init:config'... 
      Created "config/config.json" 
      Finished 'init:config' after 4.05 ms 
      Successfully created migrations folder at "/chapter-02/config
      /migrations". 
      Finished 'init:migrations' after 1.42 ms 
      Successfully created seeders folder at "/chapter-02/config
      /seeders". 
      Finished 'init:seeders' after 712 μs 
      Successfully created models folder at "/chapter-02/models". 
      Loaded configuration file "config/config.json". 
      Using environment "development". 
      Finished 'init:models' after 18 msStarting 'init'...

此命令还创建了用于存储应用程序模式的 models 目录,一个配置文件,以及用于保存程序和迁移脚本的文件夹。现在不要担心这个,我们将在下一节中查看迁移。

使用数据库凭据编辑 config.js 文件

正如我们所看到的,sequelize命令创建了许多文件,包括数据库配置文件。该文件具有应用程序数据库的示例配置。

  1. 打开config/config.json并编辑development标签,使用我们的数据库详细信息,如以下突出显示的代码:
      { 
        "development": { 
        "username": "root", 
            "password": "", 
            "database": "mvc_mysql_app", 
            "host": "127.0.0.1", 
            "port": "3306", 
            "dialect": "mysql" 
        }, 
        "test": { 
            "username": "root", 
            "password": null, 
            "database": "database_test", 
            "host": "127.0.0.1", 
            "dialect": "mysql" 
        }, 
        "production": { 
            "username": "root", 
            "password": null, 
            "database": "database_production", 
            "host": "127.0.0.1", 
            "dialect": "mysql" 
        } 
      } 

提示

请注意,我正在使用没有密码的 root 用户连接我的数据库,如果您使用不同的用户或使用不同的密码,请用您自己的凭据替换上述代码。

创建用户模式

借助Sequelize-cli,我们将为应用程序用户创建一个简单的模式:

在根项目文件夹中打开终端/Shell,并键入以下命令:

 sequelize model:create --name User --attributes "name:string,
      email:string"

您将在终端窗口上看到以下输出:

Sequelize [Node: 6.3.0, CLI: 2.3.1, ORM: 3.19.3]
Loaded configuration file "config/config.json".
Using environment "development".
Using gulpfile /usr/local/lib/node_modules/sequelize-
      cli/lib/gulpfile.js
Starting 'model:create'...
Finished 'model:create' after 13 ms

让我们检查models/User.js中的用户模型文件,这里使用define()函数添加sequelize以创建用户模式:

      'use strict'; 
      module.exports = function(sequelize, DataTypes) { 
        var User = sequelize.define('User', { 
          name: DataTypes.STRING, 
          email: DataTypes.STRING 
        },
       { 
          classMethods: { 
           associate: function(models) { 
              // associations can be defined here 
           } 
        } 
       }); 
       return User; 
       }; 

请注意,此命令在models文件夹中创建了User.js文件,并且还创建了一个包含哈希和要在数据库中执行的操作名称的迁移文件在migrations文件夹中。

该文件包含创建数据库中用户表所需的样板。

      'use strict'; 
       module.exports = { 
        up: function(queryInterface, Sequelize) { 
          return queryInterface.createTable('Users', { 
            id: { 
              allowNull: false, 
              autoIncrement: true, 
              primaryKey: true, 
              type: Sequelize.INTEGER 
            }, 
            name: { 
                type: Sequelize.STRING 
            }, 
            email: { 
              type: Sequelize.STRING 
            }, 
            createdAt: { 
              allowNull: false, 
              type: Sequelize.DATE 
            }, 
            updatedAt: { 
               allowNull: false, 
               type: Sequelize.DATE 
            } 
          }); 
        }, 
        down: function(queryInterface, Sequelize) { 
            return queryInterface.dropTable('Users'); 
        } 
      }; 

创建乐队模式

让我们创建一个模式,将在数据库中存储用户在系统中创建的每个乐队的数据。

  1. 打开终端/Shell 并键入以下命令:
 sequelize model:create --name Band --attributes "name:string,
       description:string, album:string, year:string, UserId:integer"

  1. 与上一步一样,创建了两个文件,一个用于迁移数据,另一个用作乐队模型,如下所示的代码:
      'use strict'; 
      module.exports = function(sequelize, DataTypes) { 
        var Band = sequelize.define('Band', { 
          name: DataTypes.STRING, 
          description: DataTypes.STRING, 
          album: DataTypes.STRING, 
          year: DataTypes.STRING, 
          UserId: DataTypes.INTEGER 
        }, { 
          classMethods: { 
             associate: function(models) { 
              // associations can be defined here 
          } 
        } 
      }); 
      return Band; 
      }; 

在乐队和用户模型之间创建关联

在使用方案迁移脚本之前的最后一步,我们需要创建用户模型和乐队模型之间的关联。我们将使用以下关联:

模型 关联
Band.js Band.belongsTo(models.User);
User.js User.hasMany(models.Band);

提示

您可以在以下链接找到有关关联的更多信息:docs.sequelizejs.com/en/latest/docs/associations/

  1. 打开User.js模型并添加以下突出显示的代码:
      'use strict'; 
       module.exports = function(sequelize, DataTypes) { 
         var User = sequelize.define('User', { 
           name: DataTypes.STRING, 
           email: DataTypes.STRING 
         }, { 
           classMethods: { 
             associate: function(models) { 
              // associations can be defined here 
              User.hasMany(models.Band); 
            } 
          } 
         }); 
       return User; 
      }; 

  1. 打开Band.js模型并添加以下突出显示的代码:
      'use strict'; 
       module.exports = function(sequelize, DataTypes) { 
         var Band = sequelize.define('Band', { 
           name: DataTypes.STRING, 
           description: DataTypes.STRING, 
           album: DataTypes.STRING, 
           year: DataTypes.STRING, 
           UserId: DataTypes.INTEGER 
         }, { 
           classMethods: { 
              associate: function(models) { 
              // associations can be defined here 
        Band.belongsTo(models.User); 
         } 
        } 
       }); 
       return Band; 
      }; 

在 Mysql 上创建数据库

在尝试访问 Mysql 控制台之前,请确保它正在运行。要检查:

  1. 打开终端/ shell 并使用以下命令登录您的 Mysql:
 mysql -u root

  1. 请记住,如果您使用不同的用户名或密码,请使用以下命令并将youruseryourpassword替换为您自己的凭据:
 mysql -u youruser -p yourpassword

  1. 现在让我们创建我们的数据库,输入以下命令:
 CREATE DATABASE mvc_mysql_app;

  1. 命令执行后的结果将是以下行:
 Query OK, 1 row affected (0,04 sec)

这证实了操作是成功的,我们准备继续前进。

使用数据库迁移在 Mysql 上插入数据

现在是将模式迁移到数据库的时候了。再次使用sequelize-cli进行此迁移。在继续之前,我们需要手动安装一个 Mysql 模块。

  1. 打开终端/ shell 并输入以下命令:
 npm install

提示

请注意,Sequelize接口取决于应用程序中使用的每种类型数据库的各个模块,我们的情况下是使用 Mysql

  1. 打开您的终端/ shell 并输入以下命令:
 sequelize db:migrate

  1. 这将是上述操作的结果,您终端的输出:
Sequelize [Node: 6.3.0, CLI: 2.3.1, ORM: 3.19.3, mysql: ².10.2]
Loaded configuration file "config/config.json".
Using environment "development".
Using gulpfile /usr/local/lib/node_modules/sequelize-
      cli/lib/gulpfile.js
Starting 'db:migrate'...
Finished 'db:migrate' after 438 ms
== 20160319100145-create-user: migrating =======
== 20160319100145-create-user: migrated (0.339s)
== 20160319101806-create-band: migrating =======
== 20160319101806-create-band: migrated (0.148s)

检查数据库表

我们可以使用自己的 Mysql 控制台来查看表是否成功创建。但是我将使用另一个具有图形界面的功能,它极大地简化了工作,因为它允许更快速、更轻松地显示,并且可以快速对基础数据进行操作。

由于我正在使用 Mac OSX,我将使用一个名为Sequel Pro的应用程序,这是一个免费且轻量级的应用程序,用于管理 Mysql 数据库。

提示

您可以在以下链接找到有关Sequel Pro的更多信息:www.sequelpro.com/

前面的命令:sequelize db:migrate创建了表,如我们在以下图中所见:

  1. 这张图片显示了左侧选择的乐队表,右侧显示了我们在乐队模式上设置的属性的内容:检查数据库表

乐队表

  1. 这张图片显示了左侧选择的SequelizeMeta表,右侧显示了config/migrations文件夹中生成的Sequelize文件的内容:检查数据库表

迁移文件

  1. 这张图片显示了左侧选择的用户表,右侧显示了我们在用户模式上设置的属性的内容:检查数据库表

用户表

SquelizeMeta表以与我们在迁移文件夹中的迁移文件相同的方式保存迁移文件。

现在我们已经为数据库中的数据插入创建了必要的文件,我们准备继续创建应用程序的其他文件。

创建应用程序控制器

下一步是为模型 User 和 Band 创建控件:

  1. controllers文件夹中,创建一个名为User.js的新文件并添加以下代码:
      var models = require('../models/index'); 
      var User = require('../models/user'); 

      // Create Users 
      exports.create = function(req, res) { 
          // create a new instance of the Users model with request body 
          models.User.create({ 
            name: req.body.name, 
              email: req.body.email 
          }).then(function(user) { 
              res.json(user); 
          }); 
       }; 

       // List Users 
       exports.list = function(req, res) { 
           // List all users 
           models.User.findAll({}).then(function(users) { 
               res.json(users); 
          }); 
      }; 

提示

请注意,文件的第一行导入了index模型;这个文件是创建所有控件的基础,它是用于映射其他模型的sequelize

  1. controllers文件夹中为 Band 控制器做同样的事情;创建一个名为Band.js的文件并添加以下代码:
      var models = require('../models/index'); 
      var Band = require('../models/band'); 

      // Create Band 
      exports.create = function(req, res) { 
          // create a new instance of the Bands model with request body 
          models.Band.create(req.body).then(function(band) { 
              //res.json(band); 
              res.redirect('/bands'); 
          }); 
      }; 

      // List Bands 
      exports.list = function(req, res) { 
          // List all bands and sort by Date 
          models.Band.findAll({ 
            // Order: lastest created 
              order: 'createdAt DESC' 
          }).then(function(bands) { 
               //res.json(bands); 
              // Render result 
               res.render('list', { 
                  title: 'List bands', 
                  bands: bands 
               }); 
           });  
      }; 

      // Get by band id 
      exports.byId = function(req, res) { 
          models.Band.find({ 
             where: { 
               id: req.params.id 
            } 
          }).then(function(band) { 
              res.json(band); 
          }); 
       } 
       // Update by id 
       exports.update = function (req, res) { 
           models.Band.find({ 
           where: { 
              id: req.params.id 
           } 
        }).then(function(band) { 
             if(band){ 
               band.updateAttributes({ 
                  name: req.body.name, 
                  description: req.body.description, 
                  album: req.body.album, 
                  year: req.body.year, 
                  UserId: req.body.user_id 
               }).then(function(band) { 
                  res.send(band); 
              }); 
            } 
          }); 
      } 

      // Delete by id 
      exports.delete = function (req, res) { 
          models.Band.destroy({ 
            where: { 
               id: req.params.id 
            } 
          }).then(function(band) { 
              res.json(band); 
          }); 
      } 

  1. 现在让我们重构index.js控制器并添加以下代码:
      // List Sample Bands 
      exports.show = function(req, res) { 
         // List all comments and sort by Date 
         var topBands = [ 
              { 
                  name: 'Motorhead', 
                  description: 'Rock and Roll Band', 
                  album: 'http://s2.vagalume.com/motorhead/discografia
                  /orgasmatron-W320.jpg', year:'1986', 
              }, 
              { 
                  name: 'Judas Priest', 
                  description: 'Heavy Metal band', 
                  album: 'http://s2.vagalume.com/judas-priest/discografia
                   /screaming-for-vengeance-W320.jpg', year:'1982', 
              }, 
              { 
                  name: 'Ozzy Osbourne', 
                  description: 'Heavy Metal Band', 
                  album: 'http://s2.vagalume.com/ozzy-osbourne/discografia
                  /diary-of-a-madman-W320.jpg', year:'1981', 
              } 
         ]; 
           res.render('index', { 
               title: 'The best albums of the eighties', 
               callToAction: 'Please be welcome, click the button below 
               and register your favorite album.', bands: topBands 
           }); 
      }; 

请注意,使用前面的代码,我们只是创建了一个简单的列表,以在主屏幕上显示一些专辑。

创建应用程序模板/视图

现在让我们创建应用程序视图:

  1. views/pages文件夹中,创建一个名为band-list.html的新文件并添加以下代码:
      {% extends 'layout.html' %} 
      {% block title %}{% endblock %} 
      {% block content %} 
      <div class="album text-muted"> 
      <div class="container"> 
      <div class="row"> 
                  {% for band in bands %} 
      <div class="card col-lg-4"> 
      <h2 class="text-lg-center">{{ band.name }}</h2> 
                          {% if band.album == null %} 
       <img src="img/320x320" alt="{{ band.name }}"
        style="height: 320px; width: 100%; display: block;"> 
                          {% endif %} 
                          {% if band.album %} 
       <img src="img/{{ band.album }}" width="100%" height="320px"> 
                          {% endif %} 
       <p class ="card-text">{{ band.description }}</p> 
       </div> 
                   {% endfor %} 
       </div> 
       </div> 
       </div> 
       {% endblock %} 

  1. 打开views/pages/index.html并添加以下代码:
      {% extends 'layout.html' %} 
      {% block title %}{% endblock %} 
      {% block content %}  
      <section class="jumbotron text-xs-center"> 
      <div class="container"> 
        <h1 class="jumbotron-heading">{{ title }}</h1> 
        <p class="lead text-muted">{{ callToAction }}</p> 
        <p> 
        <a href="/bands" class="btn btn-secondary">
          View Full List Albums</a> 
        </p> 
      </div> 
      </section> 
      <div class="album text-muted"> 
        <div class="container"> 
          <div class="row"> 
                  {% for band in bands %} 
          <div class="card col-lg-4"> 
            <h2 class="text-lg-center">{{ band.name }}</h2> 
                          {% if band.album == null %} 
            <img src="img/320x320" alt="{{ band.name }}"
              style="height: 320px; width: 100%; display: block;"> 
                      {% endif %} 
                      {% if band.album %} 
            <img src="img/{{ band.album }}" width="100%" height="320px"> 
                      {% endif %} 
            <p class="card-text">{{ band.description }}</p> 
          </div> 
                  {% endfor %} 
          </div> 
        </div> 
      </div> 
      {% endblock %} 

  1. 打开views/pages/layou.html并添加以下突出显示的代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
          {% include "../partials/head.html" %} 
      </head> 
      <body>
 <div class="navbar-collapse inverse collapse" id="navbar-header"
       aria-expanded="false" style="height: 0px;"> 
      <div class="container-fluid"> 
      <div class="about"> 
        <h4>About</h4> 
        <p class="text-muted">Add some information about the album below,
           the author, or any other background context. Make it a few
           sentences long so folks can pick up some informative tidbits.
           Then, link them off to some social networking sites or contact
           information.
         </p> 
      </div> 
      <div class="social"> 
      <h4>Contact</h4> 
      <ul class="list-unstyled"> 
        <li><a href="#">Follow on Twitter</a></li> 
        <li><a href="#">Like on Facebook</a></li> 
        <li><a href="#">Email me</a></li> 
      </ul> 
      </div> 
      </div> 
      </div> 
      <div class="navbar navbar-static-top navbar-dark bg-inverse"> 
      <div class="container-fluid"> 
        <button class="navbar-toggler collapsed" type="button"
          data-toggle="collapse" data-target="#navbar-header"
          aria-expanded="false"> 
        </button> 
        <a href="/" class="navbar-brand">MVC MySql App</a> 
      </div> 
      </div> 

              {% block content %} 
              {% endblock %} 
      <footer class="text-muted"> 
      <div class="container"> 
        <p class="pull-xs-right"> 
        <a href="#">Back to top</a>  
        </p> 
        <p>Sample Page using Album example from © Bootstrap!</p> 
        <p>New to Bootstrap? <a href="http://v4-alpha.getbootstrap.
            com/getting-started/introduction/">Visit the homepage
        </a>.</p> 
      </div> 
      </footer>
           {% include "../partials/footer.html" %} 
      </body> 
      </html> 

为应用程序添加样式

我们还将在public/stylesheet文件中添加一些 CSS 行来为我们的示例应用程序设置样式。

打开public/stylesheets/style.css并添加以下代码:

  body { 
    min-height: 75rem; /* Can be removed; just added for demo purposes */ 
  } 
  .navbar { 
    margin-bottom: 0; 
  } 
  .navbar-collapse .container-fluid { 
    padding: 2rem 2.5rem; 
    border-bottom: 1px solid #55595c; 
  } 
  .navbar-collapse h4 { 
    color: #818a91; 
  } 
  .navbar-collapse .text-muted { 
    color: #818a91; 
  } 
  .about { 
    float: left; 
    max-width: 30rem; 
    margin-right: 3rem; 
  } 
  .social a { 
  font-weight: 500; 
    color: #eceeef; 
  } 
  .social a:hover { 
    color: #fff; 
  } 
  .jumbotron { 
    padding-top: 6rem; 
    padding-bottom: 6rem; 
    margin-bottom: 0; 
    background-color: #fff; 
  } 
  .jumbotron p:last-child { 
    margin-bottom: 0; 
  } 
  .jumbotron-heading { 
    font-weight: 300; 
  } 
  .jumbotron .container { 
    max-width: 45rem; 
  } 
  .album { 
    min-height: 50rem; /* Can be removed; just added for demo purposes */ 
    padding-top: 3rem; 
    padding-bottom: 3rem; 
    background-color: #f7f7f7; 
  } 
  .card { 
    float: left; 
    width: 33.333%; 
    padding: .75rem; 
    margin-bottom: 2rem; 
    border: 0; 
  } 
  .card > img { 
    margin-bottom: .75rem; 
  } 
  .card-text { 
    font-size: 85%; 
  } 
  footer { 
    padding-top: 3rem; 
    padding-bottom: 3rem; 
  } 
  footer p { 
    margin-bottom: .25rem; 
  } 

添加路由和应用程序控制器

我们将编辑app.js文件以向band-list.html视图添加路由,以及它们各自的控制器:

  1. 打开app.js并在索引控制器导入后添加以下行:
      // Inject band controller 
      var bands = require('./controllers/band'); 
      // Inject user controller 
      var users = require('./controllers/user'); 

  1. 在索引路由app.get('/', index.show);后添加以下代码:
      // Defining route to list and post 
      app.get('/bands', bands.list); 
      // Get band by ID 
      app.get('/band/:id', bands.byId); 
      // Create band 
      app.post('/bands', bands.create); 
      // Update 
      app.put('/band/:id', bands.update); 
      // Delete by id 
      app.delete('/band/:id', bands.delete); 
      // Defining route to list and post users 
      app.get('/users', users.list); 
      app.post('/users', users.create); 

此时,我们几乎完成了应用程序的所有工作; 让我们在浏览器上检查结果。

  1. 打开您的终端/ shell,并键入以下命令:
 npm start 

  1. 打开浏览器并转到此 URL:http://localhost:3000/

结果将是以下截图:

添加路由和应用程序控制器

主屏幕的索引模板

如果我们检查http://localhost:3000/bands上的 Band 路由,我们将看到一个空屏幕,http://localhost:3000/users也是一样,但在这里我们找到了一个空的JSON数组。

让我们为 Band 的路由添加一些内容。

添加数据库内容

让我们在数据库中添加一些内容:

  1. 创建一个名为mvc_mysql_app.sql的新文件,并放入以下代码:
      # Dump of table Bands 
      # ------------------------------------------------------------ 

      DROP TABLE IF EXISTS `Bands`; 

      CREATE TABLE `Bands` ( 
        `id` int(11) NOT NULL AUTO_INCREMENT, 
        `name` varchar(255) DEFAULT NULL, 
        `description` varchar(255) DEFAULT NULL, 
        `album` varchar(255) DEFAULT NULL, 
        `year` varchar(255) DEFAULT NULL, 
        `UserId` int(11) DEFAULT NULL, 
        `createdAt` datetime NOT NULL, 
        `updatedAt` datetime NOT NULL, 
        PRIMARY KEY (`id`) 
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

      LOCK TABLES `Bands` WRITE; 
      /*!40000 ALTER TABLE `Bands` DISABLE KEYS */; 

      INSERT INTO `Bands` (`id`, `name`, `description`, `album`, `year`,
      `UserId`, `createdAt`, `updatedAt`) 
      VALUES 
         (2,'Motorhead','Rock and Roll Band','http://s2.vagalume.com/
          motorhead/discografia/ace-of-spades-W320.jpg','1979',NULL,
          '2016-03-13 21:50:25','2016-03-12 21:50:25'), 
         (4,'Black Sabbath','Heavy Metal Band','http://s2.vagalume.com/
          black-sabbath/discografia/heaven-and-hell W320.jpg','1980',
          NULL,'2016-03-12 22:11:00','2016-03-12 23:08:30'), 
         (6,'Deep Purple','Heavy Metal band','http://s2.vagalume.com
          /deep-purple/discografia/perfect-strangersW320.jpg',
           '1988',NULL,'2016-03-13 23:09:59','2016-03-12 23:10:29'), 
         (7,'White Snake','Heavy Metal band','http://s2.vagalume.com/
           whitesnake/discografia/slip-of-the-tongueW320.jpg','1989',
            NULL,'2016-03-13 01:58:56','2016-03-13 01:58:56'),
         (8,'Iron maiden','Heavy Metal band','http://s2.vagalume.com/
            iron-maiden/discografia/the-number-of-the-beastW320.jpg',
            '1982',NULL,'2016-03-13 02:01:24','2016-03-13 02:01:24'),
         (9,'Queen','Heavy Metal band','http://s2.vagalume.com/queen
            /discografia/greatest-hits-vol-1-W320.jpg','1981',NULL,
            '2016-03-13 02:01:25','2016-03-13 02:01:25'); 

       /*!40000 ALTER TABLE `Bands` ENABLE KEYS */; 
       UNLOCK TABLES; 

       /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 
       /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 
       /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 
       /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 
       /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 
       /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 

  1. 打开Sequel Pro,单击文件 > 导入 >,然后选择 SQL 文件mvc_mysql_app.sql

  2. 返回浏览器并刷新http://localhost:3000/bands页面; 您将看到以下结果:添加数据库内容

Band-list.html

创建一个乐队表单

现在我们将使用模态功能 bootstrap 创建乐队创建表单:

  1. 打开views/pages/index.html文件,并在文件末尾添加以下代码:
      <div class="modal fade" id="createBand" tabindex="-1" role="dialog" 
         aria-labelledby="myModalLabel" aria-hidden="true"> 
      <div class="modal-dialog" role="document"> 
      <div class="modal-content"> 
        <form action="/bands" method="post"> 
          <div class="modal-header"> 
            <button type="button" class="close" data-dismiss="modal" 
             aria-label="Close"> 
            <span aria-hidden="true">&times;</span> 
            </button> 
              <h4 class="modal-title" id="myModalLabel">Insert an
               Album</h4> 
          </div> 
          <div class="modal-body"> 
          <fieldset class="form-group"> 
          <label  for="inputname">Band Name</label> 
          <input type="text" id="inputname" name="name" 
           class="form-control" placeholder="Band name" required=""> 
         </fieldset> 
         <fieldset class="form-group"> 
         <label  for="inputdescription">Description</label> 
         <textarea id="nputdescription" name="description" rows="8"
           cols="40" class="form-control" placeholder="Description"
           required="">
         </textarea> 
         </fieldset> 
         <fieldset class="form-group">  
         <label  for="inputalbum">Best Album</label> 
         <input type="text" id="inputalbum" name="album" rows="8" cols="40"  
          class="form-control" placeholder="Link to Album cover">
           </textarea> 
         </fieldset> 
       <fieldset class="form-group"> 
         <label  for="inputyear">Release Year</label> 
         <input type="text" id="inputyear" name="year" rows="8" cols="40" 
          class="form-control" placeholder="Year" required=""></textarea> 
       </fieldset> 

      </div> 
        <div class="modal-footer"> 
          <button type="button" class="btn btn-secondary" 
            data-dismiss="modal">Close</button> 
          <button type="submit" class="btn btn-primary">Save 
            changes</button> 
       </div> 
      </form> 
      </div> 
      </div> 
      </div> 

  1. 重新启动应用程序,打开您的终端/ shell,并键入以下命令:
 npm start

  1. 单击插入专辑按钮,您可以在模型窗口内看到乐队表单,如下图所示:创建乐队表单

模态屏幕

插入新乐队

现在让我们检查表单的行为:

  1. 使用以下数据填写表单:
  1. 单击保存更改按钮。

表单处理后,您将被重定向到band-list.html,并显示新记录,如下图所示:

插入新乐队

带有新记录的 Band-list.html 屏幕

Band.js控制器上的create()函数通过表单POST激活,并且Band.js控制器中的以下代码用于保存数据并重定向用户:

      // Create Band 
      exports.create = function(req, res) { 
          // create a new instance of the Bands model with request body 
          models.Band.create(req.body).then(function(band) { 
              //res.json(band); 
              res.redirect('/bands'); 
          }); 
      }; 

ODM(mongoose)和 ORM(sequelize)之间的主要区别

两个数据库映射器之间的主要区别是 Sequelize 使用 promises 而 Mongoose 不使用。 Promises 易于处理异步事件。 更明确地说,让我们看一下以下代码来比较这两个中间件:

  1. 从上一章的passport.js文件中提取的代码块:
      User.findOne({ 'local.email' :  email }, function(err, user) { 
                          // if errors 
                          if (err) 
                          return done(err); 
                          // check email 
                          if (user) { 
                              return done(null, false, 
                               req.flash('signupMessage', 'Wohh! the email
                                is already taken.')); 
                               } else { 
                              // create the user 
                              var newUser = new User(); 
                              // Get user name from req.body 
                              newUser.local.name = req.body.name; 
                              newUser.local.email = email; 
                              newUser.local.password = 
                                newUser.generateHash(password); 
                              // save data 
                              newUser.save(function(err) { 
                                  if (err) 
                                  throw err; 
                                  return done(null, newUser); 
                              }); 
                           } 
               }); 

  1. 现在使用sequelize promises 函数的相同代码块:
      User.findOne({ where: { localemail: email }}) 
          .then(function(user) { 

         if (user) 
             return done(null, false, req.flash('loginMessage', 'That
              email
               is already taken.')); 
          if(req.user) { 

              var user = req.user; 
              user.localemail = email; 
              user.localpassword = User.generateHash(password); 
              user.save() 
                  .then (function() { 
                      done(null, user); 
                  }) 
                  .catch(function (err) { 
                      done(null, false, req.flash('loginMessage',
                       err));}); 
                  }); 
          } else { 
              // create the user 
              var newUser = User.build ({ 
                  localemail: email, 
                  localpassword: User.generateHash(password) 
              }); 
              // store the newUser to the database 
              newUser.save() 
                  .then(function() { 
                      done (null, newUser); 
                  }) 
                  .catch(function(err) { 
                      done(null, false, req.flash('loginMessage',
                       err));}); 
                  } 
              }) 
          .catch(function (e) { 
               done(null, false, req.flash('loginMessage',e.name + " " + 
                   e.message));                  
         }) 

请注意,使用then()函数处理所有返回。

总结

在本章中,我们探索了sequelize-CLI命令行的所有功能,以在关系数据库中创建表的映射。 我们看到了如何使用sequelize model feature create()交互式地创建模型,还看到了如何将模式文件迁移到数据库。

我们使用标准模板引擎启动了应用程序,并了解了如何重构引擎模板并使用另一个资源,即Swig模板库。

我们学习了如何使用一些 SQL 命令连接到 MySQL 数据库以及创建表的一些基本命令。

在下一章中,我们将探索使用 Node.js 和其他重要资源来利用和操作图像。

第三章:构建多媒体应用程序

在 Node.js 应用程序中最常讨论的话题之一无疑是文件的加载和存储,无论是文本、图像、音频还是视频。也有许多方法可以做到这一点;我们不会深入技术细节,但会简要概述两种最重要的方法。

一种是直接以二进制格式保存文件在数据库中,另一种方式是直接将文件保存在服务器上(服务器的硬盘),或者简单地将文件存储在云中。

在本章中,我们将看到一种非常实用的方式,可以直接将文件上传到硬盘,并将文件名记录在我们的数据库中作为参考。这样,如果需要,我们可以使用可扩展的云存储服务。

在本章中,我们将涵盖以下主题:

  • 如何将不同的文件上传到硬盘

  • 如何使用 Stream API 读写文件

  • 处理多部分表单上传

  • 如何配置 Multer 模块将文件存储在本地机器上

  • 如何获取文件类型并应用简单的文件验证

  • 如何使用动态用户 gravatar 生成器

我们正在构建什么?

我们将构建一个使用 MongoDB 和 Mongoose 进行用户身份验证的上传图像和视频的应用程序;然后我们可以看到这些图像将成为我们工作的最终结果。

在这个例子中,我们将使用另一种方式开始我们的项目;这次我们将从package.json文件开始。

以下截图展示了我们最终应用程序的样子:

图像屏幕 视频屏幕
我们正在构建什么? 我们正在构建什么?

从 package.json 开始

正如我们在前几章中所解释的,packages.json文件是应用程序的核心。创建必要文件的步骤如下:

  1. 创建一个名为chapter-03的文件夹。

  2. 创建一个名为package.json的新文件,并将其保存在chapter-03文件夹中,包含以下代码:

      { 
        "name": "chapter-03", 
        "description": "Build a multimedia Application with Node.js", 
        "license": "MIT", 
        "author": { 
          "name": "Fernando Monteiro", 
          "url": "https://github.com/newaeonweb/node-6-blueprints" 
       }, 
        "repository": { 
          "type": "git", 
          "url": "https://github.com/newaeonweb/node-6-blueprints.git" 
        }, 
        "keywords": [ 
          "MVC", 
          "Express Application", 
          "Expressjs", 
          "Expressjs images upload", 
          "Expressjs video upload" 
        ], 
        "version": "0.0.1", 
        "private": true, 
        "scripts": { 
          "start": "node app.js" 
        }, 
         "dependencies": { 
          "bcrypt-nodejs": "0.0.3", 
          "body-parser": "~1.13.2", 
          "connect-flash": "⁰.1.1", 
          "connect-mongo": "¹.1.0", 
          "cookie-parser": "~1.3.5", 
          "debug": "~2.2.0", 
          "ejs": "~2.3.3", 
           "express": "~4.13.1", 
          "express-session": "¹.13.0", 
          "gravatar": "¹.4.0", 
          "mongoose": "⁴.4.5", 
          "morgan": "~1.6.1", 
          "multer": "¹.1.0", 
          "node-sass-middleware": "0.8.0", 
          "passport": "⁰.3.2", 
          "passport-local": "¹.0.0", 
          "serve-favicon": "~2.3.0" 
        }, 
        "devDependencies": { 
          "nodemon": "¹.9.1" 
        } 
      } 

添加基线配置文件

现在让我们向项目添加一些有用的文件:

  1. 创建一个名为.editorconfig的文件,并将其保存在chapter-03文件夹中,包含以下代码:
      # http://editorconfig.org 
      root = true 

      [*] 
      indent_style = tab 
      indent_size = 4 
      charset = utf-8 
      trim_trailing_whitespace = true 
      insert_final_newline = true 

      [*.md] 
      trim_trailing_whitespace = false 

  1. 创建一个名为.gitignore的文件,保存在chapter-03中,并包含以下代码:
      # Logs 
      logs 
      *.log 

      # Runtime data 
      pids 
      *.pid 
      *.seed 

      # Directory for instrumented libs generated by jscoverage/JSCover 
      lib-cov 

      # Coverage directory used by tools like istanbul 
          coverage 

      # Grunt intermediate 
      storage (http://gruntjs.com/creating-plugins#storing-task-files) 
      .grunt 

      # node-waf configuration 
      .lock-wscript 

      # Compiled binary addons (http://nodejs.org/api/addons.html) 
      build/Release 

      # Dependency directory 
      # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-
      my-node_modules-folder-into-git- 
      node_modules 

      # Debug log from npm 
      npm-debug.log 

提示

请记住,我们正在使用 git 作为源代码控制。虽然这个文件不是运行应用程序所必需的,但我们强烈建议您使用源代码版本控制系统。

  1. 创建一个名为app.js的文件。

添加服务器文件夹

要完成应用程序的基本创建,我们现在将创建存储控件、模板和应用程序其他文件的目录:

  1. 创建一个名为public的文件夹,并在其中创建以下文件夹:
  • /images

  • /javascripts

  • /stylesheets

  • /uploads

  • /视频

  1. 创建一个名为server的文件夹,并在其中创建这些文件夹:
  • /config

  • /controllers

  • /models

  • /views

  1. 此时,我们的项目已经具有了所有基本目录和文件;让我们从package.json中安装 Node 模块。

  2. 在项目根目录打开您的终端/ shell,并输入以下命令:

npm install

在执行步骤 1、2 和 3 之后,项目文件夹将具有以下结构:

添加服务器文件夹

文件夹结构

让我们开始创建app.js文件内容。

配置 app.js 文件

我们将逐步创建app.js文件;它将与第一章中创建的应用程序有许多相似的部分。但是,在本章中,我们将使用不同的模块和不同的方式来创建应用程序控件。

Node.js中使用 Express 框架,有不同的方法来实现相同的目标:

  1. 打开项目根目录下的app.js文件,并添加以下模块:
      // Import basic modules 
      var express = require('express'); 
      var path = require('path'); 
      var favicon = require('serve-favicon'); 
      var logger = require('morgan'); 
      var cookieParser = require('cookie-parser'); 
      var bodyParser = require('body-parser'); 

      // import multer 
      var multer  = require('multer'); 
      var upload = multer({ dest:'./public/uploads/', limits: {fileSize:
       1000000, files:1} }); 

提示

请注意,我们正在使用 Multer 模块来处理multipart/form-data。您可以在github.com/expressjs/multer找到有关multer的更多信息。

  1. multer导入之后添加以下行:
       // Import home controller 
       var index = require('./server/controllers/index'); 
       // Import login controller 
       var auth = require('./server/controllers/auth'); 
       // Import comments controller 
       var comments = require('./server/controllers/comments'); 
       // Import videos controller 
       var videos = require('./server/controllers/videos'); 
       // Import images controller 
       var images = require('./server/controllers/images'); 

提示

不要担心这些控制文件。在本书的后面,我们会逐个看到它们。此时,我们将专注于创建app.js

  1. controller导入器之后添加以下行:
       // ODM with Mongoose 
       var mongoose = require('mongoose'); 
       // Modules to store session 
       var session = require('express-session'); 
       var MongoStore = require('connect-mongo')(session); 
       // Import Passport and Warning flash modules 
       var passport = require('passport'); 
       var flash = require('connect-flash'); 
       // start express application in variable app. 
       var app = express(); 

上述代码设置了带有消息系统的用户会话,还使用 Passport 进行身份验证。

  1. 现在让我们设置模板引擎和与应用程序数据库的连接。在express app变量之后添加以下代码:
      // view engine setup 
      app.set('views', path.join(__dirname, 'server/views/pages')); 
      app.set('view engine', 'ejs'); 
      // Database configuration 
      var config = require('./server/config/config.js'); 
      // connect to our database 
      mongoose.connect(config.url); 
       // Check if MongoDB is running 
     mongoose.connection.on('error', function() { 
         console.error('MongoDB Connection Error. Make sure MongoDB is
         running.'); 
     }); 
     // Passport configuration 
     require('./server/config/passport')(passport); 

以下行设置了一些默认中间件并初始化了Passport-local和用户会话。

  1. 在上一个代码块之后添加以下行:
      app.use(logger('dev')); 
      app.use(bodyParser.json()); 
      app.use(bodyParser.urlencoded({ extended: false })); 
      app.use(cookieParser()); 
      app.use(require('node-sass-middleware')({ 
      src: path.join(__dirname, 'public'), 
      dest: path.join(__dirname, 'public'), 
      indentedSyntax: true, 
      sourceMap: true 
      })); 
      // Setup public directory 
      app.use(express.static(path.join(__dirname, 'public'))); 

      // required for passport 
      // secret for session 
      app.use(session({ 
      secret: 'sometextgohere', 
      saveUninitialized: true, 
      resave: true, 
           //store session on MongoDB using express-session + connect mongo 
      store: new MongoStore({ 
      url: config.url, 
      collection : 'sessions' 
          }) 
      })); 

      // Init passport authentication 
     app.use(passport.initialize()); 
      // persistent login sessions 
      app.use(passport.session()); 
      // flash messages 
      app.use(flash()); 

现在让我们添加所有应用程序路由。我们本可以使用外部文件来存储所有路由,但是我们将其保留在此文件中,因为我们的应用程序中不会有太多路由。

  1. app.use(flash())之后添加以下代码:
      // Application Routes 
      // Index Route 
      app.get('/', index.show); 
      app.get('/login', auth.signin); 
      app.post('/login', passport.authenticate('local-login', { 
         //Success go to Profile Page / Fail go to login page 
      successRedirect : '/profile', 
      failureRedirect : '/login', 
      failureFlash : true 
      })); 
      app.get('/signup', auth.signup); 
      app.post('/signup', passport.authenticate('local-signup', { 
          //Success go to Profile Page / Fail go to Signup page 
      successRedirect : '/profile', 
      failureRedirect : '/signup', 
      failureFlash : true 
      })); 

      app.get('/profile', auth.isLoggedIn, auth.profile); 

      // Logout Page 
      app.get('/logout', function(req, res) { 
      req.logout(); 
      res.redirect('/'); 
       }); 

      // Setup routes for comments 
      app.get('/comments', comments.hasAuthorization, comments.list); 
      app.post('/comments', comments.hasAuthorization, comments.create); 

      // Setup routes for videos 
      app.get('/videos', videos.hasAuthorization, videos.show); 
      app.post('/videos', videos.hasAuthorization, upload.single('video'),
      videos.uploadVideo); 

      // Setup routes for images 
      app.post('/images', images.hasAuthorization, upload.single('image'),
      images.uploadImage); 
      app.get('/images-gallery', images.hasAuthorization, images.show); 

提示

在这里,您可以看到我们正在使用第一章中的示例应用程序相同的路由和函数,使用 MVC 设计模式构建类似 Twitter 的应用程序;我们保留了路由、身份验证和评论,并进行了一些小的更改。

要采取的最后一步是添加error函数并配置我们的应用程序将使用的服务器端口

  1. 在上一个代码块之后添加以下代码:
      // catch 404 and forward to error handler 
      app.use(function(req, res, next) { 
      var err = new Error('Not Found'); 
      err.status = 404; 
      next(err); 
       }); 

      // development error handler 
      // will print stacktrace 
      if (app.get('env') === 'development') { 
      app.use(function(err, req, res, next) { 
      res.status(err.status || 500); 
      res.render('error', { 
      message: err.message, 
      error: err 
              }); 
         }); 
      } 

      // production error handler 
      // no stacktraces leaked to user 
      app.use(function(err, req, res, next) { 
      res.status(err.status || 500); 
      res.render('error', { 
       message: err.message, 
      error: {} 
          }); 
      }); 

      module.exports = app; 

      app.set('port', process.env.PORT || 3000); 

      var server = app.listen(app.get('port'), function() { 
      console.log('Express server listening on port ' +
      server.address().port); 
      }); 

创建 config.js 文件

server/config/文件夹内创建一个名为config.js的文件,并添加以下代码:

// Database URL 
module.exports = { 
    'url' : 'mongodb://localhost/mvc-app-multimedia' 
}; 

server/config/文件夹内创建一个名为passport.js的文件。

在这一步中,我们将使用第一章中使用的Passport模块的相同配置文件,在 Node.js 中使用 MVC 设计模式构建类似 Twitter 的应用程序。您可以从www.packtpub.com或从 GitHub 的官方存储库下载示例代码。

创建控制器文件

现在让我们在server/controllers中创建控制器文件:

  1. 创建一个名为auth.js的文件,并添加以下代码:
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 
      var passport = require('passport'); 

      // Signin GET 
      exports.signin = function(req, res) { 
          // List all Users and sort by Date 
      res.render('login', { title: 'Login Page', message:
      req.flash('loginMessage') }); 
      }; 
      // Signup GET 
      exports.signup = function(req, res) { 
          // List all Users and sort by Date 
      res.render('signup', { title: 'Signup Page', message:
      req.flash('signupMessage') }); 

      }; 
      // Profile GET 
      exports.profile = function(req, res) { 
          // List all Users and sort by Date 
      res.render('profile', { title: 'Profile Page', user : req.user,
      avatar:gravatar.url(req.user.email ,  {s: '100', r: 'x', d: 'retro'},
      true) }); 
      }; 
      // Logout function 
      exports.logout = function () { 
      req.logout(); 
      res.redirect('/'); 
      }; 

      // check if user is logged in 
      exports.isLoggedIn = function(req, res, next) { 
      if (req.isAuthenticated()) 
      return next(); 
      res.redirect('/login'); 
      }; 

  1. 创建一个名为comments.js的文件,并添加以下代码:
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 
      // get Comments model 
      var Comments = require('../models/comments'); 

      // List Comments 
      exports.list = function(req, res) { 
         // List all comments and sort by Date 
      Comments.find().sort('-created').populate('user',
      'local.email').exec(function(error, comments) { 
       if (error) { 
       returnres.send(400, { 
       message: error 
                  }); 
              } 
              // Render result 
       res.render('comments', { 
       title: 'Comments Page', 
       comments: comments, 
       gravatar: gravatar.url(comments.email ,  {s: '80', r: 'x', d:
        'retro'}, true) 
               }); 
        }); 
      }; 
      // Create Comments 
      exports.create = function(req, res) { 
         // create a new instance of the Comments model with request body 
      var comments = new Comments(req.body); 
          // Set current user (id) 
      comments.user = req.user; 
          // save the data received 
      comments.save(function(error) { 
      if (error) { 
      returnres.send(400, { 
       message: error 
                  }); 
              } 
              // Redirect to comments 
      res.redirect('/comments'); 
           }); 
      }; 
      // Comments authorization middleware 
       exports.hasAuthorization = function(req, res, next) { 
         if (req.isAuthenticated()) 
         return next(); 
        res.redirect('/login'); 
      }; 

  1. 创建一个名为index.js的文件,并添加以下代码:
      // Show home screen 
      exports.show = function(req, res) { 
         // Render home screen 
       res.render('index', { 
               title: 'Multimedia Application', 
               callToAction: 'An easy way to upload and manipulate files
                 with Node.js' 
         }); 
      }; 

现在让我们创建控制器来处理图像和视频上传到我们的应用程序。我们将使用 Node.js API 流来读取和写入我们的文件。

提示

您可以在nodejs.org/api/stream.html找到此 API 的完整文档。

  1. 创建一个名为images.js的文件。

  2. 添加以下代码:

      // Import modules 
      var fs = require('fs'); 
      var mime = require('mime'); 
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 

      var Images = require('../models/images'); 
      // set image file types 
      var IMAGE_TYPES = ['image/jpeg','image/jpg', 'image/png']; 

  1. importer模块之后添加以下代码:
      // Show images gallery 
      exports.show = function (req, res) { 

      Images.find().sort('-created').populate('user',
      'local.email').exec(function(error, images) { 
      if (error) { 
        returnres.status(400).send({ 
          message: error 
         }); 
      } 
      // Render galley 
      res.render('images-gallery', {  
      title: 'Images Gallery', 
      images: images, 
      gravatar: gravatar.url(images.email ,  {s: '80', r: 'x', d: 'retro'},
      true) 
              }); 
          }); 
      }; 

现在让我们添加负责将文件保存到临时目录并将其传输到public文件夹中的目录的函数。

  1. 在上一个代码块之后添加以下行:
      // Image upload 
      exports.uploadImage = function(req, res) { 
      var src; 
      var dest; 
      var targetPath; 
      var targetName; 
      var tempPath = req.file.path; 
      console.log(req.file); 
          //get the mime type of the file 
      var type = mime.lookup(req.file.mimetype); 
         // get file extension 
      var extension = req.file.path.split(/[. ]+/).pop(); 
         // check support file types 
      if (IMAGE_TYPES.indexOf(type) == -1) { 
        returnres.status(415).send('Supported image formats: jpeg, jpg,
        jpe, png.'); 
       } 
          // Set new path to images 
      targetPath = './public/images/' + req.file.originalname; 
         // using read stream API to read file 
      src = fs.createReadStream(tempPath); 
         // using a write stream API to write file 
      dest = fs.createWriteStream(targetPath); 
      src.pipe(dest); 

          // Show error 
      src.on('error', function(err) { 
      if (err) { 
        returnres.status(500).send({ 
        message: error 
                  }); 
              } 
       }); 
          // Save file process 
      src.on('end', function() { 
             // create a new instance of the Images model with request body 
      var image = new Images(req.body); 
             // Set the image file name 
      image.imageName = req.file.originalname; 
             // Set current user (id) 
      image.user = req.user; 
             // save the data received 
      image.save(function(error) { 
      if (error) { 
        return res.status(400).send({ 
        message: error 
                      }); 
                  } 
              }); 
              // remove from temp folder 
      fs.unlink(tempPath, function(err) { 
      if (err) { 
          return res.status(500).send('Woh, something bad happened here'); 
                } 
                // Redirect to galley's page 
          res.redirect('images-gallery'); 

              }); 
          }); 
      }; 

添加检查用户是否经过身份验证并被授权插入图像的函数。

  1. 在文件末尾添加以下代码:
      // Images authorization middleware 
      exports.hasAuthorization = function(req, res, next) { 
      if (req.isAuthenticated()) 
      return next(); 
      res.redirect('/login'); 
      }; 

  1. 现在让我们重复这个过程来控制videos.js,然后创建一个名为videos.js的文件,并添加以下代码:
      // Import modules 
      var fs = require('fs'); 
      var mime = require('mime'); 
      // get gravatar icon from email 
      var gravatar = require('gravatar'); 

      // get video model 
      var Videos = require('../models/videos'); 
      // set image file types 
      var VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg',
       'video/ogv']; 

      // List Videos 
      exports.show = function(req, res) { 

      Videos.find().sort('-created').populate('user',
      'local.email').exec(function(error, videos) { 
      if (error) { 
        return res.status(400).send({ 
        message: error 
                  }); 
              } 
              // Render result 
      console.log(videos); 
      res.render('videos', { 
      title: 'Videos Page', 
      videos: videos, 
      gravatar: gravatar.url(videos.email ,  {s: '80', r: 'x', d: 'retro'},
      true) 
              }); 
          }); 
      }; 
      // Create Videos 
      exports.uploadVideo = function(req, res) { 
      var src; 
      var dest; 
      var targetPath; 
      var targetName; 
      console.log(req); 
      var tempPath = req.file.path; 
          //get the mime type of the file 
      var type = mime.lookup(req.file.mimetype); 
         // get file extenstion 
      var extension = req.file.path.split(/[. ]+/).pop(); 
        // check support file types 
      if (VIDEO_TYPES.indexOf(type) == -1) { 
      return res.status(415).send('Supported video formats: mp4, webm, ogg,
      ogv'); 
          } 
          // Set new path to images 
      targetPath = './public/videos/' + req.file.originalname; 
         // using read stream API to read file 
      src = fs.createReadStream(tempPath); 
        // using a write stream API to write file 
      dest = fs.createWriteStream(targetPath); 
      src.pipe(dest); 

          // Show error 
      src.on('error', function(error) { 
      if (error) { 
        return res.status(500).send({ 
        message: error 
                  }); 
              } 
        }); 

          // Save file process 
      src.on('end', function() { 
              // create a new instance of the Video model with request body 
      var video = new Videos(req.body); 
              // Set the video file name 
      video.videoName = req.file.originalname; 
             // Set current user (id) 
      video.user = req.user; 
             // save the data received 
      video.save(function(error) { 
      if (error) { 
        return res.status(400).send({ 
        message: error 
                      }); 
                 } 
              }); 
              // remove from temp folder 
      fs.unlink(tempPath, function(err) { 
      if (err) { 
        return res.status(500).send({ 
        message: error 
                      }); 
                  } 
                 // Redirect to galley's page 
      res.redirect('videos'); 

              }); 
          }); 
      }; 
      // Videos authorization middleware 
      exports.hasAuthorization = function(req, res, next) { 
      if (req.isAuthenticated()) 
         return next(); 
         res.redirect('/login'); 
      };

如您所见,我们使用与图像控制器相同的概念来创建视频控制器。

由于流,Node.js API 可以使用createReadStream()createWriteStream()函数处理任何类型的文件。

创建模型文件

现在让我们创建应用程序的模板文件。由于我们在第一章中使用了 mongoose 中间件,在 Node.js 中使用 MVC 设计模式构建类似 Twitter 的应用程序,我们将保持相同类型的配置:

  1. server/models文件夹内创建一个名为comments.js的文件,并添加以下代码:
      // load the things we need 
      var mongoose = require('mongoose'); 
      var Schema = mongoose.Schema; 

      var commentSchema = mongoose.Schema({ 
      created: { 
       type: Date, 
      default: Date.now 
          }, 
      title: { 
      type: String, 
      default: '', 
      trim: true, 
      required: 'Title cannot be blank' 
          }, 
      content: { 
      type: String, 
      default: '', 
      trim: true 
          }, 
      user: { 
      type: Schema.ObjectId, 
      ref: 'User' 
          } 
      }); 

      module.exports = mongoose.model('Comments', commentSchema); 

  1. server/models文件夹内创建一个名为user.js的文件,并添加以下代码:
      // Import Mongoose and password Encrypt 
      var mongoose = require('mongoose'); 
      var bcrypt   = require('bcrypt-nodejs'); 

      // define the schema for User model 
      var userSchema = mongoose.Schema({ 
          // Using local for Local Strategy Passport 
      local: { 
      name: String, 
      email: String, 
      password: String, 
          } 

      }); 

      // Encrypt Password 
      userSchema.methods.generateHash = function(password) { return
      bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 
      }; 

      // Verify if password is valid 
      userSchema.methods.validPassword = function(password) { return
      bcrypt.compareSync(password, this.local.password); 
      }; 

      // create the model for users and expose it to our app 
      module.exports = mongoose.model('User', userSchema); 

  1. 然后,在server/models文件夹内创建一个名为images.js的文件,并添加以下代码:
      // load the things we need 
      var mongoose = require('mongoose'); 
      var Schema = mongoose.Schema; 

      var imagesSchema = mongoose.Schema({ 
      created: { 
      type: Date, 
      default: Date.now 
          }, 
      title: { 
      type: String, 
      default: '', 
      trim: true, 
      required: 'Title cannot be blank' 
          }, 
      imageName: { 
      type: String 
          }, 
      user: { 
      type: Schema.ObjectId, 
      ref: 'User' 
          } 
      }); 

      module.exports = mongoose.model('Images', imagesSchema); 

  1. 接下来,在server/models文件夹内创建一个名为videos.js的文件,并添加以下代码:
      // load the things we need 
      var mongoose = require('mongoose'); 
      var Schema = mongoose.Schema; 

      var videosSchema = mongoose.Schema({ 
        created: { 
          type: Date, 
          default: Date.now 
        }, 
        title: { 
          type: String, 
          default: '', 
          trim: true, 
          required: 'Title cannot be blank' 
        }, 
        videoName: { 
         type: String 
        }, 
        user: { 
          type: Schema.ObjectId, 
          ref: 'User' 
        } 
      }); 

      module.exports = mongoose.model('Videos', videosSchema); 

创建视图文件

在本节中,我们将使用与第一章中相同的view文件来处理以下文件:

  • views/partials/javascripts.ejs

  • views/partials/stylesheets.ejs

  • views/pages/login.ejs

  • views/pages/signup.ejs

  • views/pages/profile.ejs

  • views/pages/index.ejs

  • views/pages/comments.js

  • views/pages/error.ejs

如前所述,您可以从 Packt 网站或本书的官方 GitHub 存储库下载这些文件。

除了这些文件之外,我们将为照片和视频页面创建views文件,并将这些路由添加到应用程序菜单中:

  1. views/partials文件夹内创建一个名为footer.ejs的文件,并添加以下代码:
      <footer class="footer"> 
      <div class="container"> 
       <span>&copy 2016\. Node-Express-MVC-Multimedia-App</span> 
      </div> 
      </footer> 

  1. 然后在views/partials文件夹内创建一个名为header.ejs的文件,并添加以下代码:
      <!-- Fixed navbar --> 
      <div class="pos-f-t"> 
        <div class="collapse" id="navbar-header"> 
          <div class="container bg-inverse p-a-1"> 
            <h3>Collapsed content</h3> 
            <p>Toggleable via the navbar brand.</p> 
          </div> 
        </div> 
        <nav class="navbarnavbar-light navbar-static-top"> 
        <div class="container"> 
          <button class="navbar-toggler hidden-sm-up" type="button"
          data-toggle="collapse" data-target="#exCollapsingNavbar2"> 
          Menu 
          </button> 
          <div class="collapse navbar-toggleable-xs"
            id="exCollapsingNavbar2"> 
            <a class="navbar-brand" href="/">MVC Multimedia App</a> 
            <ul class="navnavbar-navnavbar-right"> 
              <li class="nav-item"> 
                <a class="nav-link" href="/login">Sign in</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/signup">Sign up</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/profile">Profile</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/comments">Comments</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/videos">Videos</a> 
              </li> 
              <li class="nav-item"> 
                <a class="nav-link" href="/images-gallery">Photos</a> 
              </li> 
            </ul> 
          </div> 
        </div> 
        </nav> 
      </div> 
      <!-- Fixed navbar --> 

  1. views/pages文件夹内创建一个名为images-gallery.ejs的文件,并添加以下代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
        <title><%= title %></title> 
        <% include ../partials/stylesheet %> 
      </head> 
      <body> 
        <% include ../partials/header %> 
        <div class="container"> 
          <div class="row"> 
            <div class="col-lg-6"> 
              <h4 class="text-muted">Images</h4> 
            </div> 
          <div class="col-lg-6"> 
            <button type="button" class="btnbtn-secondary pull-right"
             data-toggle="modal" data-target="#createImage"> 
                    Image Upload 
             </button> 
           </div> 
         </div> 
         <!-- Modal --> 
         <div class="modal fade" id="createImage" tabindex="-1"
          role="dialog" aria-labelledby="myModalLabel"
          aria-hidden="true"> 
           <div class="modal-dialog" role="document"> 
             <div class="modal-content"> 
               <form action="/images" method="post"
                 enctype="multipart/formdata"> 
               <div class="modal-header"> 
                 <button type="button" class="close" data-dismiss="modal"
                  aria-label="Close"> 
                   <span aria-hidden="true">&times;</span> 
                 </button> 
                 <h4 class="modal-title" id="myModalLabel">
                   Upload a imagefile
                 </h4>
               </div> 

               <div class="modal-body"> 
                 <fieldset class="form-group"> 
                 <label  for="itle">Title</label> 
                   <input type="text" id="itle" name="title" class="form-
                    control" placeholder="Image Title" required=""> 
                 </fieldset> 
                 <label class="file" style="width: 100%"> 
                   <input type="file" id="image" name="image"> 
                   <span class="file-custom"></span> 
                 </label> 
               </div> 
               <div class="modal-footer"> 
                 <button type="button" class="btnbtnsecondary" data
                  dismiss="modal">Close
                  </button> 
                 <button type="submit" class="btnbtn-primary">
                    Savechanges
                 </button> 
               </div> 
               </form> 
             </div> 
           </div> 
         </div> 
         <hr> 
         <div class="row"> 
           <% images.forEach(function(images){ %> 
           <div class="col-lg-4"> 
             <figure class="figure"> 
              <img src="img/<%= images.imageName %>" class="figure-img
               img-fluid img-rounded" alt="<%= images.imageName %>"> 
              <figcaption class="figure-caption"><%= images.title%>
              </figcaption> 
              <small>Upload by: <%= images.user.local.email %></small> 
             </figure> 
           </div> 
           <% }); %> 
         </div> 
       </div> 
       <% include ../partials/footer %> 
       <% include ../partials/javascript %> 
      </body> 
      </html> 

上述代码设置了一个表单 HTML 标签,使用enctype="multipart/form-data类型,并创建一个循环来显示添加到图库中的所有图像。

  1. views/pages文件夹内创建一个名为videos.ejs的文件,并添加以下代码:
      <!DOCTYPE html> 
      <html> 
      <head> 
        <title><%= title %></title> 
        <% include ../partials/stylesheet %> 
      </head> 
      <body> 
      <% include ../partials/header %> 
      <div class="container"> 
      <div class="row"> 
      <div class="col-lg-6"> 
      <h4 class="text-muted">Videos</h4> 
      </div> 
      <div class="col-lg-6"> 
      <button type="button" class="btn btn-secondary pull-right"
       data-toggle="modal" data-target="#createVideo"> 
                    Video Upload 
       </button> 
      </div> 
      </div> 
      <!-- Modal --> 
      <div class="modal fade" id="createVideo" tabindex="-1" role="dialog"
       aria-labelledby="myModalLabel" aria-hidden="true"> 
      <div class="modal-dialog" role="document"> 
      <div class="modal-content">  
      <form action="/videos" method="post" enctype="multipart/form-data"> 
      <div class="modal-header"> 
      <button type="button" class="close" data-dismiss="modal"
       aria-label="Close"> 
      <span aria-hidden="true">&times;</span> 
      </button> 
      <h4 class="modal-title" id="myModalLabel">Upload a video file</h4> 
       </div> 

      <div class="modal-body"> 
      <fieldset class="form-group"> 
      <label  for="inputitle">Title</label> 
      <input type="text" id="inputitle" name="title" class="form-control"
       placeholder="Video Title" required=""> 
      </fieldset> 
       <label class="file" style="width: 100%"
         onclick="$('input[id=lefile]').click();"> 
       <input type="file" id="video" name="video"> 
       <span class="file-custom"></span> 
       </label> 
       </div> 
      <div class="modal-footer"> 
      <button type="button" class="btnbtn-secondary"
       data-dismiss="modal">Close</button> 
      <button type="submit" class="btnbtn-primary">Save changes</button> 
      </div> 
      </form> 
      </div> 
      </div> 
      </div> 
      <hr> 
      <div class="row"> 
      <% videos.forEach(function(videos){ %> 
      <div class="col-lg-4"> 
      <h4 class="list-group-item-heading"><%= videos.title %></h4> 
      <video width="320" height="240" controls preload="auto"
      codecs="avc1.42E01E, mp4a.40.2"> 
      <source src="img/<%= videos.videoName %>" type="video/mp4" /> 
      </video> 
      <small>Upload by: <%= videos.user.local.email %></small> 
       </div> 
      <% }); %> 
      </div> 
      </div> 
      <% include ../partials/footer %> 
      <% include ../partials/javascript %> 
      </body> 
      </html> 

创建 public 文件夹内容

在这个阶段,我们已经完成了在server目录中创建文件夹和文件的所有必要步骤,如控制器、模型和视图。

现在我们需要复制在第一章中创建的public文件夹中的内容:

  1. 复制以下文件夹及其内容,并将它们粘贴到chapter-03根项目文件夹中:
  • public/images

  • public/javascripts

  • bootstrap.min.js

  • jquery.min.js

  • public/stylesheets

  • bootstrap.min.css

  • style.css

  • style.css.map

  • style.sass

  1. public文件夹内创建一个名为uploads的文件夹。

  2. 然后,在public文件夹内创建一个名为videos的文件夹。

使用上传表单在应用程序中插入图像

现在是测试应用程序的时候了,需要注意的是,为此您应该启动您的 MongoDB。否则,应用程序在连接时会返回失败错误:

  1. 在项目根目录打开您的终端/Shell,并输入以下命令:
npm start

  1. 转到http://localhost:3000/signup并输入以下数据:
  • 姓名:约翰

  • 电子邮件:john@doe.com

  • 密码:123

  1. 转到http://localhost:3000/images-gallery,点击图像上传按钮,填写表单标题并选择图像(请注意,我们设置了图像大小限制为1MB,仅用于示例目的)。您将看到一个模型表单,如下截图所示:使用上传表单在应用程序中插入图像

图像上传表单

  1. 选择图像后,点击保存更改按钮,完成!您将在http://localhost:3000/images-gallery页面看到以下截图:使用上传表单在应用程序中插入图像

图库图像屏幕

使用上传表单在应用程序中插入视频文件

与插入图像到我们的应用程序一样,让我们按照相同的步骤来插入视频:

  1. 转到http://localhost:3000/videos,点击视频上传按钮,填写表单标题并选择视频文件(请注意,我们再次将图像大小限制设置为 1MB,视频格式设置为MP4, WEBM,仅用于示例目的)。您将看到一个模型表单,如下截图所示:使用上传表单在应用程序中插入视频文件

视频上传表单

关于图像和视频上传的重要说明

Node.js 为我们提供了一个完整的处理文件的 API(流 API),如图像、视频、pdf 和其他格式。还有几种将文件上传到服务器或存储在云中的方法,正如前面已经提到的。此外,Node.js 生态系统为我们提供了许多模块,用于处理不同类型的文件,并使用带有enctype="multipart/form-data"的表单。

在本章中,我们使用了Multer模块。Multer 是一个完整的中间件,用于处理各种上传和文件存储的方法。

在这个例子中,我们只在 MongoDB 中存储了文件名,并直接将文件发送到服务器文件夹。还有另一种上传方式,我们将文件以二进制格式发送到数据库,尽管重要的是要记住 MongoDB 存储BSON文件的容量为16MB。

如果您选择将文件存储在 MongoDB 数据库中,您可以使用 MongoDB 的 GridFS 功能和 Node.js 模块,如GridFS-stream,作为上传中间件,就像我们使用 Multer 一样。

在本章的示例中,我们将上传大小限制为 1 MB,在下面突出显示的行中可以看到:

      var upload = multer({ 
         dest:'./public/uploads/', 
         limits: {fileSize: 1000000, files:1} 
      }); 

提示

您可以在 Multer 的官方文档中找到有关限制的更多信息,网址为github.com/expressjs/multer#limits

总结

在本章中,我们构建了一个完整的 Node MVC 应用程序,用于上传图像和视频文件;我们还设置了一个用户会话,使用电子邮件和密码进行身份验证,并将加密密码存储在 MongoDB 上。

此外,本章使您能够构建模块化、健壮和可扩展的 MVC 应用程序。

此外,我们还使用了在 Web 应用程序中非常常见的功能,如访问数据库、登录和注册以及文件上传。

在下一章中,我们将看到如何使用云服务操纵和上传图像,并将元数据存储在 MongoDB 中。

第四章:不要拍照,制作它-摄影师的应用程序

在本章中,我们将讨论 Node.js 社区在全球范围内广泛讨论的一个话题-使用 Node.js 架构和云进行图像处理。

正如我们在上一章中看到的,我们有两种方式来存储图像和文件,一种是在服务器上使用硬盘,另一种是直接存储到云中。在第三章中,构建多媒体应用程序,我们使用了直接上传到服务器的方法,将图像存储在硬盘上。

在本章中,我们将使用云中的服务器来存储和处理我们相册应用程序中的图像。

我们将使用一个名为 Cloudinary 的服务来存储和处理图像。

在本章中,我们将涵盖以下主题:

  • 如何使用 generator-express 设置 MVC 应用程序

  • 如何安装 cloudinary npm 模块

  • 实现 Materialize.css 框架

  • 如何上传图像到云并在 MongoDB 上保存元数据

  • 如何使用点文件中的全局变量

  • 设置 Cloudinary 帐户并创建文件夹

  • 如何使用 Cloudinary API 上传图像

  • 如何使用 Cloudinary API 中的 URL 参数呈现模板

我们正在构建的内容

在本章结束时,我们将创建以下示例应用程序,一个强大而可扩展的相册:

我们正在构建的内容

相册应用程序的主屏幕

创建基线应用程序

在本章中,我们将使用稍微修改过的express-generator的版本,这是我们在之前章节中使用的。

这个生成器叫做generator-express;它在express generator的基础上进行了大量的改进。

以下是我们安装的步骤:

  1. 打开终端/ shell 并输入:
 npm install -g generator-express 

  1. 创建一个名为chapter04的文件夹。

  2. chapter04文件夹中打开您的终端/ shell,并输入以下命令:

 yo express 

现在,按照以下顺序填写问题:

  • 选择N,我们已经在步骤 2中创建了项目文件夹

  • 选择MVC作为应用程序类型

  • 选择Swig作为模板引擎

  • 选择None作为 CSS 预处理器

  • 选择None作为数据库(在本章后面,我们将手动设置数据库)

  • 选择Gulp进行 LiveReload 和其他内容

提示

不要担心Gulp,如果你从未听说过它。在本书的后面,我们将看到并解释一些构建工具。

在生成器的最后,我们有以下目录结构:

创建基线应用程序

应用程序文件夹结构

更改应用程序结构

与我们在第一章中使用的示例不同,使用 MVC 设计模式构建类似 Twitter 的应用程序,我们不会对当前的结构进行重大更改;我们只会更改views文件夹。

作为示例应用程序,将有一本图片书;我们将在views文件夹中添加一个名为 book 的文件夹:

  1. app/views文件夹中创建一个名为book的文件夹。

  2. 现在我们将为 Cloudinary 服务创建一个配置文件。在本章后面,我们将讨论有关 Cloudinary 的所有细节;现在,只需创建一个新文件。

  3. 在根文件夹中创建一个名为.env的文件。

现在,我们有必要的基础来继续前进。

添加处理图像和 Cloudinary 云服务的 Node 模块

现在我们将在package.json文件中为我们的应用程序添加必要的模块。

  1. 将以下突出显示的代码行添加到package.json文件中:
      { 
         "name": "chapter-04", 
         "description": "Don't take a photograph, make it - An app for
           photographers", 
         "license": "MIT", 
         "author": { 
           "name": "Fernando Monteiro", 
           "url": "https://github.com/newaeonweb/node-6-blueprints" 
         }, 
         "repository": { 
           "type": "git", 
           "url": "https://github.com/newaeonweb/node-6-blueprints.git" 
         }, 
         "keywords": [ 
           "MVC", 
           "Express Application", 
           "Expressjs", 
           "Expressjs cloud upload", 
           "Expressjs cloudinary upload" 
         ], 
         "version": "0.0.1", 
         "private": true, 
        "scripts": { 
          "start": "gulp" 
        }, 
        "dependencies": { 
          "body-parser": "¹.13.3", 
          "cloudinary": "¹.3.1", 
          "compression": "¹.5.2", 
          "connect-multiparty": "².0.0", 
          "cookie-parser": "¹.3.3", 
          "dotenv": "².0.0", 
          "express": "⁴.13.3", 
          "glob": "⁶.0.4", 
          "jugglingdb": "².0.0-rc3", 
          "jugglingdb-mongodb": "⁰.1.1", 
          "method-override": "².3.0", 
          "morgan": "¹.6.1", 
          "serve-favicon": "².3.0", 
          "swig": "¹.4.2" 
        }, 
        "devDependencies": { 
          "gulp": "³.9.0", 
          "gulp-nodemon": "².0.2", 
          "gulp-livereload": "³.8.0", 
          "gulp-plumber": "¹.0.0" 
        } 
      } 

只需几个模块,我们就可以构建一个非常强大和可扩展的应用程序。让我们描述每一个:

模块名称 描述 更多信息
cloudinary 用于存储和管道图像和视频文件的云服务 www.npmjs.com/package/cloudinary
connect-multiparty 用于接受多部分表单上传的中间件 www.npmjs.com/package/connect-multiparty
dotenv 加载环境变量 www.npmjs.com/package/dotenv
jugglingdb 跨数据库 ORM www.npmjs.com/package/jugglingdb
jugglingdb-mongodb MongoDB 连接器 www.npmjs.com/package/jugglingdb-mongodb

创建书籍控制器

我们将遵循生成器建议的相同生成器代码模式;使用此生成器的优势之一是我们已经可以使用 MVC 模式。

提示

请记住,您可以从 Packpub 网站或直接从 GitHub 书库下载示例文件。

  1. controllers文件夹中创建一个名为books.js的文件。

  2. 将以下代码添加到book.js文件中:

      var express = require('express'), 
          router = express.Router(), 
          schema = require('../models/book'), 
          Picture = schema.models.Picture, 
          cloudinary = require('cloudinary').v2, 
          fs = require('fs'),     
          multipart = require('connect-multiparty'), 
          multipartMiddleware = multipart(); 

      module.exports = function (app) { 
          app.use('/', router); 
      }; 
      // Get pictures list 
      router.get('/books', function (req, res, next) { 
          Picture.all().then(function (photos) { 
              console.log(photos); 
          res.render('book/books', { 
              title: 'PhotoBook', 
              photos: photos, 
              cloudinary: cloudinary 
          }) 
          }); 
       }); 
       // Get form upload 
       router.get('/books/add', function (req, res, next) { 
           res.render('book/add-photo', { 
           title: 'Upload Picture' 
        }); 
      }); 
      // Post to 
      router.post('/books/add', multipartMiddleware, function (req, res,
       next)
      {
          // Checking the file received 
          console.log(req.files); 
          // create a new instance using Picture Model 
          var photo = new Picture(req.body); 
          // Get temp file path 
          var imageFile = req.files.image.path; 
          // Upload file to Cloudinary 
          cloudinary.uploader.upload(imageFile, { 
              tags: 'photobook', 
              folder: req.body.category + '/', 
              public_id: req.files.image.originalFilename 
              // eager: { 
              //   width: 280, height: 200, crop: "fill", gravity: "face" 
             // } 
          }) 
            .then(function (image) { 
              console.log('Picture uploaded to Cloudinary'); 
              // Check the image Json file 
              console.dir(image); 
              // Added image informations to picture model 
              photo.image = image; 
              // Save photo with image metadata 
              return photo.save(); 
            }) 
            .then(function (photo) { 
                console.log('Successfully saved'); 
                // Remove image from local folder 
                var filePath = req.files.image.path; 
                fs.unlinkSync(filePath); 
            }) 
            .finally(function () { 
                // Show the result with image file 
                res.render('book/posted-photo', { 
                    title: 'Upload with Success', 
                    photo: photo, 
                    upload: photo.image 
                }); 
            }); 
      }); 

让我们解释一些关于前面代码示例的重要点:

  • 为了在视图中使用 Cloudinary API,我们需要将cloudinary变量传递给我们的视图:
      res.render('book/books', { 
                title: 'PhotoBook', 
                photos: photos, 
                cloudinary: cloudinary 
      }) 

  • 在使用multipartMiddleware时,为了最佳实践,我们需要清理上传到云中的每个文件:

      .then(function (photo) { 
            console.log('Successfully saved'); 
            // Remove image from local folder 
            var filePath = req.files.image.path; 
            fs.unlinkSync(filePath); 
      }) 

稍后我们将讨论更多关于 Cloudinary API 的内容。

提示

请注意,当您使用多部分连接时,默认情况下会将图像加载到硬盘上的文件夹中,因此您应该始终删除应用程序中加载的所有文件。

创建书籍模型文件

为此应用程序创建模型的过程与我们在前几章中看到的非常相似;几乎每个模块的ORM/ODM都有非常相似的操作。

让我们看看如何为书籍对象创建模型:

  1. app/models文件夹中创建一个名为book.js的文件,并放入以下代码:
      var Schema = require('jugglingdb').Schema; 
      // Pay attention, we are using MongoDB for this example. 
      var schema = new Schema('mongodb', {url: 'mongodb://localhost
      /photobookapp'}); 

      // Setup Books Schema 
      var Picture = schema.define('Picture', { 
        title : { type: String, length: 255 }, 
        description: {type: Schema.Text}, 
        category: {type: String, length: 255 }, 
        image : { type: JSON} 
      }); 

      module.exports = schema; 

提示

请注意,我们使用 MongoDB 来存储书籍模型。还记得在启动应用程序之前必须使本地 MongoDB 运行起来。

向应用程序添加 CSS 框架

在本书的所有章节中,我们将始终使用最新的技术,就像在前几章中使用新的 Bootstrap(Alpha Release)一样。

特别是在本章中,我们将使用一种称为Material Design的设计模式。

提示

您可以在www.google.com/design/spec/material-design/introduction.html上阅读更多关于设计材料的信息。

为此,我们将使用一个名为Materialize.css的简单CSS框架。

提示

您可以在此链接找到有关 Materialize 的更多信息:materializecss.com/

  1. 用以下代码替换app/views/layout.swig文件中的所有内容:
      <!doctype html> 
      <html lang="en"> 
      <head> 
         <meta charset="UTF-8"> 
         <meta name="viewport" content="width=device-width"> 
         <title>{{ title }}</title> 
         <!--Let browser know website is optimized for mobile--> 
         <meta name="viewport" content="width=device-width, initial-
         scale=1.0"/>
         <!-- Import Google Material font and icons -->
         <link href="https://fonts.googleapis.com/icon?family=
         Material+Icons" rel="stylesheet"> 
          <!-- Compiled and minified CSS --> 
          <link rel="stylesheet" href="https://cdnjs.cloudflare.com/
          ajax/libs/materialize/0.97.6/css/materialize.min.css"> 
         <link rel="stylesheet" href="/css/style.css"> 
     </head> 
     <body> 
          <nav class="orange darken-4" role="navigation"> 
              <div class="nav-wrapper container"><a id="logo-container"
              href="/" class="brand-logo">Logo</a> 
                  <ul class="right hide-on-med-and-down"> 
                      <li><a href="/books">Books</a></li> 
                      <li><a href="/books/add">Add Picture</a></li> 
                  </ul> 

                  <ul id="nav-mobile" class="side-nav" style="transform:
                   translateX(-100%);"> 
                      <li><a href="/books">Books</a></li> 
                      <li><a href="/books/add">Add Picture</a></li> 
                  </ul> 
                  <a href="#" data-activates="nav-mobile" class="button-
                  collapse">
                  <i class="material-icons">menu</i></a> 
              </div> 
          </nav> 
          {% block content %}{% endblock %} 
          <!-- Footer --> 
          <footer class="page-footer orange darken-4"> 
              <div class="container"> 
                  <div class="row"> 
                     <div class="col l6 s12"> 
                          <h5 class="white-text">Some Text Example</h5> 
                          <p class="grey-text text-lighten-4">Lorem ipsum
                          dolor sit amet, consectetur adipiscing elit,
                          sed do eiusmod tempor incididunt ut labore et
                          dolore magnaaliqua. Ut enim ad minim veniam, quis
                          nostrud exercitation ullamco laboris nisi ut 
                          aliquip ex ea commodo consequat. Duis aute irure 
                          dolor in reprehenderit in voluptate velit esse 
                          cillum dolore eu fugiat nulla pariatur.</p> 
                      </div> 
                      <div class="col l3 s12"> 
                          <h5 class="white-text">Sample Links</h5> 
                          <ul> 
                            <li><a class="white-text" href="#!">Link 1
                              </a></li> 
                            <li><a class="white-text" href="#!">Link 2
                               </a></li> 
                            <li><a class="white-text" href="#!">Link 3
                               </a></li>
                            <li><a class="white-text" href="#!">Link 4
                               </a></li> 
                          </ul> 
                      </div> 
                    <div class="col l3 s12"> 
                        <h5 class="white-text">Sample Links</h5> 
                         <ul> 
                            <li><a class="white-text" href="#!">Link 1
                              </a></li> 
                            <li><a class="white-text" href="#!">Link 2
                              </a></li> 
                            <li><a class="white-text" href="#!">Link 3
                              </a></li>
                            <li><a class="white-text" href="#!">Link 4
                              </a></li>
                          </ul> 
                    </div> 
                </div> 
            </div> 
              <div class="footer-copyright"> 
                <div class="container"> 
                  MVC Express App for: <a class="white-text text-darken-2"
                  href="#">Node.js 6 Blueprints Book</a> 
                </div> 
              </div> 
          </footer> 
          <!-- Place scripts at the bottom page--> 
          {% if ENV_DEVELOPMENT %} 
          <script src="img/livereload.js"></script> 
          {% endif %} 

          <!--Import jQuery before materialize.js--> 
          <script type="text/javascript"
            src="img/jquery-2.1.1.min.js"></script> 
          <!-- Compiled and minified JavaScript --> 
          <script src="https://cdnjs.cloudflare.com/
          ajax/libs/materialize/0.97.6/js/materialize.min.js"></script> 
          <!-- Init Rsponsive Sidenav Menu  --> 
          <script> 
          (function($){ 
              $(function(){ 
                $('.button-collapse').sideNav(); 
                $('.materialboxed').materialbox(); 
              }); 
          })(jQuery); 
          </script> 
      </body> 
      </html> 

提示

为了避免 CSS 冲突,请清理您的public/css/style.css文件。

重构视图文件夹

现在我们将对app/views文件夹进行一些小改动并添加一些文件:

  1. 首先,让我们编辑app/views/index.js。用以下代码替换原始代码:
      {% extends 'layout.swig' %} 

      {% block content %} 

      <div class="section no-pad-bot" id="index-banner"> 
        <div class="container"> 
              <br><br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center">
                <h5 class="header col s12 light">Welcome to 
                    {{ title }}
                </h5> 
              </div> 
              <div class="row center"> 
                  <a href="books/add" id="download-button" class="btn-large
                   waves-effect waves-light orange">Create Your Photo
                   Book
                  </a> 
              </div> 
              <br><br> 
          </div> 
      </div> 
      <div class="container"> 
          <div class="section"> 

              <!--   Icon Section   --> 
              <div class="row"> 
                <div class="col s12 m4"> 
                  <div class="icon-block"> 
                    <h5 class="center">Animals</h5> 
                    <img src="img/animals"/> 
                    <p class="light">Lorem ipsum dolor sit amet,
                      consectetur adipiscing elit, sed do eiusmod
                      tempor incididunt ut laboreet dolore magna-
                      aliqua.</p> 
                  </div> 
                </div> 

                  <div class="col s12 m4"> 
                    <div class="icon-block"> 
                      <h5 class="center">Cities</h5> 
                      <img src="img/city"/> 
                      <p class="light">Lorem ipsum dolor sit amet,
                        consectetur adipiscing elit, sed do eiusmod
                        tempor incididunt ut laboreet dolore magna-
                        aliqua.</p> 
                    </div> 
                  </div> 

                  <div class="col s12 m4"> 
                    <div class="icon-block"> 
                      <h5 class="center">Nature</h5> 
                      <img src="img/nature"/> 
                      <p class="light">Lorem ipsum dolor sit amet,
                        consectetur adipiscing elit, sed do eiusmod
                         tempor incididunt ut laboreet dolore magna-
                          aliqua..</p> 
                    </div> 
                  </div> 
              </div> 

          </div> 
          <br><br> 

          <div class="section"> 

          </div> 
      </div> 

      {% endblock %} 

  1. 创建一个名为add-photo.swig的文件,并添加以下代码:
      {% extends '../layout.swig' %} 

      {% block content %} 
      <div class="section no-pad-bot" id="index-banner"> 
          <div class="container"> 
              <br>
              <br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center"> 
                  <h5 class="header col s12 light">Welcome to 
                    {{ title }}</h5> 
              </div> 

              <div class="photo"> 
                <h2>{{ photo.title }}</h2> 
                {% if photo.image.url %} 
                <img src="img/{{ photo.image.url }}" height='200' width='100%'>
                </img> 
                <a href="{{ photo.image.url }}" target="_blank"> 
                  {{ cloudinary.image(photo.image.public_id, {width: 150,
                   height: 150, quality:80,crop:'fill',format:'png',
                   class:'thumbnail inline'})  }} 
                </a> 
                {% endif %} 
              </div> 

              <div class="card"> 
                  <div class="card-content orange-text"> 
                   <form action="/books/add" enctype="multipart/form-data"
                     method="POST"> 
                          <div class="input-field"> 
                             <input id="title" name="title" type="text"
                               value="{{ photo.title }}" class="validate"> 
                              <label for="title">Image Title</label> 
                          </div> 
                          <div class="file-field input-field"> 
                              <div class="btn orange"> 
                                  <span>Choose File</span> 
                                  <input type="file" name="image"> 
                                <input id="photo_image_cache"
                                 name="image_cache" type="hidden" /> 
                              </div> 
                              <div class="file-path-wrapper"> 
                               <input class="file-path validate"
                                 type="text"> 
                              </div> 
                          </div> 
                          <div class="input-field col s12"> 
                              <select class="browser-default" id="category"
                               name="category"> 
                                <option value="" disabled selected>Choose a
                                  category</option> 
                                <option value="animals">Animals</option> 
                                <option value="cities">Cities</option> 
                               <option value="nature">Nature</option> 
                               </select> 
                          </div> 
                          <div class="input-field"> 
                              <input id="description" name="description"
                               type="text" value="{{ photo.description }}"
                                class="validate"> 
                              <label for="description">Image Text
                                 Description</label> 
                          </div> 
                          <br> 
                          <br> 
                          <div class="row center"> 
                              <button class="btn orange waves-effect waves
                               light" type="submit" name="action">
                                  Submit
                               </button> 
                          </div> 
                      </form> 
                  </div> 
              </div> 
              <br> 
              <br> 
              <br> 
          </div> 
      </div> 

      {% endblock %} 

  1. 然后创建一个名为books.swig的文件,并添加以下代码:
      {% extends '../layout.swig' %} 

      {% block content %} 
      <div class="section no-pad-bot" id="index-banner"> 
          <div class="container"> 
              <br><br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center"> 
               < h5 class="header col s12 light">Welcome to {{ title }}
               </h5> 
              </div> 
              <br><br> 

              {% if photos.length == 0 %} 
                <div class="row center"> 
                    <div class="card-panel orange lighten-2">No photos yet,
                      click add picture to upload</div> 
                </div> 
                {% endif %} 

              <div class="row"> 
              {% for item in photos %} 
                  <div class="col s12 m4"> 
                      <div class="icon-block"> 
                          <h5 class="center">{{ item.title }}</h5> 
                            {{ cloudinary.image(item.image.public_id, { 
                              width:280, height: 200, quality:80, 
                               crop:'fill',format:'png', effect:
                               'brightness:20', radius: 5, class:
                                'materialboxed' }) | safe }} 
                          {# 
                            Swig comment tag 
                            <img class="materialboxed" src="
                            {{ item.image.url }}" height='200' 
                             width='100%' alt="{{ item.title }}"
                             data-caption="{{item.description}}"></img> 
                          #} 
                          <p class="light">{{ item.description }}</p> 
                      </div> 
                  </div> 
              {% endfor %} 
              </div> 
          </div> 
      </div> 
      {% endblock %} 

  1. 创建一个名为posted-photo.swig的文件,并添加以下代码:
      {% extends '../layout.swig' %} 

      {% block content %} 
      <div class="section no-pad-bot" id="index-banner"> 
          <div class="container"> 
              <br><br> 
              <h1 class="header center orange-text">{{ title }}</h1> 
              <div class="row center"> 
                  <h5 class="header col s12 light">Welcome to 
                    {{ title }}
                  </h5> 
              </div> 
              <div class="photo col s12 m12"> 
                <h2>{{ photo.title }}</h2> 
                {% if photo.image.url %} 
                <img src="img/{{ photo.image.url }}" width='100%'></img> 
                <a href="{{ photo.image.url }}" target="_blank"> 
                  {{ cloudinary.image(photo.image.public_id, {width: 150,
                    height: 150, quality: 80,crop:'fill',format:'png', 
                    class:'thumbnail inline'})  }} 
                </a> 
                {% endif %} 
              </div> 
              <br> 
              <br> 
             <br> 
          </div> 
       </div> 

      {% endblock %} 

创建.env.js 文件

在这一点上,我们将创建env.js文件;此文件验证了 Cloudinary 服务的配置。在config文件夹中创建一个名为env.js的文件,并放入以下代码:

// get Env variable / cloudinary 
module.exports = function(app, configEnv) { 

    var dotenv = require('dotenv'); 
    dotenv.load(); 
    var cloudinary = require('cloudinary').v2; 
    // Log some messages on Terminal 
    if ( typeof(process.env.CLOUDINARY_URL) == 'undefined' ){ 
      console.log('Cloudinary config file is not defined'); 
      console.log('Setup CLOUDINARY_URL or use dotenv mdule file') 
    } else { 
      console.log('Cloudinary config, successfully used:'); 
      console.log(cloudinary.config()) 
    } 
} 

现在我们有一个完全配置的应用程序;但是,我们仍然需要在 Cloudinary 服务上创建一个帐户。

创建和配置 Cloudinary 账户

Cloudinary 是用于存储和处理图像和视频文件的云服务;您可以在cloudinary.com找到有关 Cloudinary 服务的更多信息:

  1. 转到cloudinary.com/users/register/free并注册一个免费帐户。

提示

在注册表单的末尾,您可以为您的云设置一个名称。我们选择了n6b(Node.js 6 蓝图);选择您自己的名称。

  1. 从您的帐户中复制数据(环境变量)并将其直接放到仪表板面板上,如下面的屏幕截图所示:创建和配置 Cloudinary 帐户

Cloudinary 仪表板面板

  1. 现在在.env.js文件中使用您自己的凭据更新以下代码:
      PORT=9000
      CLOUDINARY_URL=cloudinary://82499XXXXXXXXX:dXXXXXXXXXXX@n6b

Cloudinary 的工作原理

除了我们在 Cloudinary 上存储文件之外,我们还可以使用强大的 API 来操作和转换图像,应用效果,调整大小,以及在我们的机器上不使用任何软件做更多的事情。

让我们回到books.js控制器,查看我们使用了什么。我们从 promises 函数中提取了额外的代码,以便专注于突出显示的代码行:

      cloudinary.uploader.upload(imageFile, 
            { 
              tags: 'photobook', 
              folder: req.body.category + '/', 
              public_id: req.files.image.originalFilename 
              // eager: { 
              //   width: 280, height: 200, crop: "fill", gravity: "face" 
             // } 
            }) 
              .then(function (image) { 
               ... 
             }) 
             .then(function (photo) { 
                ... 
              }) 
              .finally(function () { 
                ... 
              }); 

在这里,我们设置了一个文件夹,folder: req.body.category,用于存储我们的图像,并覆盖了默认的public_id: req.files.image.originalFilename,以使用图像名称。这是一个很好的做法,因为 API 为我们提供了一个带有随机字符串的public_id——没有错,但非常有用。例如,查看这样的链接:http://res.cloudinary.com/demo/image/upload/mydog.jpg

而不是这个:res.cloudinary.com/demo/image/upload/8jsb1xofxdqamu2rzwt9q.jpg

注释的eager属性使我们能够转换图像并生成一个带有所有急切选项的新图像。在这种情况下,我们可以保存一个宽度为280px,高度为200px的转换图像,裁剪填充内容,如果图片中有一些脸,缩略图将居中显示在脸上。这是一个非常有用的功能,可以保存图像配置文件。

您可以在上传方法上使用任何转换组合;以下是 API 返回的JSON的示例:

      { title: 'Sample01',
          description: 'Example with Transformation',
          image:
           { public_id: 'cpl6ghhoiqphyagwvbaj',
             version: 1461269043,
             signature: '59cbbf3be205d72fbf7bbea77de8e7391d333363',
             width: 845,
             height: 535,
             format: 'bmp',
             resource_type: 'image',
             created_at: '2016-04-21T20:04:03Z',
             tags: [ 'photobook' ],
             bytes: 1356814,
             type: 'upload',
             etag: '639c51691528139ae4f1ef00bc995464',
             url: 'http://res.cloudinary.com/n6b/image/upload/v146126904
                   /cpl6ghhoiqphyagwvbaj.bmp',
             secure_url: 'https://res.cloudinary.com/n6b/image/upload
               /v1461269043/cpl6ghhoiqphyagwvbaj.bmp',
               coordinates: { faces: [ [ 40, 215, 116, 158 ] ] },  original_filename: 'YhCmSuFxm0amW5TFX9FqXt3F',  eager:[ { transformation: 'c_thumb,g_face,h_100,w_150', 
			   width: 150, height: 100,url: 'http://res.cloudinary.com
               /n6b/image/upload/c_thumb,g_face,h_100,w_150/v1461269043
               /cpl6ghhoiqphyagwvbaj.bmp', secure_url:
                'https://res.cloudinary.com/n6b/image/upload
                 /c_thumb,g_face,h_100,w_150/v1461269043
                 /cpl6ghhoiqphyagwvbaj.bmp' } ] }

注意带有 URL 转换的突出显示的代码:

c_thumb,g_face,h_100,w_150

提示

您可以在以下链接找到有关 Cloudinary 上传 API 的更多信息:cloudinary.com/documentation/node_image_upload

运行应用程序

现在是时候执行应用程序并上传一些照片了:

  1. 打开您的终端/Shell 并输入以下命令:
 npm start

  1. 转到http://localhost:3000/books,您将看到以下屏幕:运行应用程序

书籍屏幕

上传和显示图像

现在让我们插入一些图像,并检查我们应用程序的行为:

  1. 转到http://localhost:3000/books/add,并填写表单:上传和显示图像

上传表单

添加以下信息:

标题:图像示例 02

文件:选择 sample02.jpg 文件。

类别:城市

描述:Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

  1. 让我们检查一下我们的 MongoDB,看看在我们继续之前发生了什么。

  2. 打开您的RoboMongo并选择第一个对象:上传和显示图像

来自 MongoDB(RoboMongo)的屏幕截图

提示

请注意,您必须从左侧面板菜单中选择正确的数据库。

  1. 当我们上传一张图片时,API 会返回一个包含与该图片相关的所有信息的 JSON。我们将这个 JSON 存储为我们的书籍模型的图像属性,存储在 MongoDB 中,正如我们在之前的图片中所看到的。

  2. 通过Sample02Sample06重复步骤 1

检查 MongoDb 图片集合

让我们在 MongoDB 上看一下图片集合:

  1. 打开 RoboMongo 并从左侧面板中选择正确的数据库。

  2. 打开collections文件夹,双击图片集合

  3. 在面板的右上方,单击以表格模式查看结果图标。

现在您可以在RoboMongo界面上看到以下屏幕截图:

检查 MongoDb 图片集合

来自图片集合的屏幕截图

在 Cloudinary 仪表板中创建文件夹

如前所述,我们设置了文件夹(folder: req.body.category)。在这种情况下,文件夹名称将是类别名称。为了更好地在云中组织我们的图像,就像我们以编程方式做的那样,我们需要直接在 Cloudinary 仪表板中创建它们:

  1. 登录到您的 Cloudinary 帐户。

  2. 转到cloudinary.com/console/media_library在 Cloudinary 仪表板中创建文件夹

创建文件夹截图

注意

不要担心 Cloudinary 仪表板上的其他图像;它们是每个帐户中的默认图像。如果您愿意,可以删除它们。

  1. 点击右侧的输入字段(文件夹名称)并创建一个名为animals的文件夹。

  2. 点击右侧的输入字段(文件夹名称)并创建一个名为cities的文件夹。

  3. 点击右侧的输入字段(文件夹名称)并创建一个名为nature的文件夹。

您可以在图像顶部看到所有创建的类别,如下面的截图所示:

在 Cloudinary 仪表板中创建文件夹

类别截图

现在当您选择一个类别时,您只会看到属于该类别的图像,例如animals,如下图所示:

在 Cloudinary 仪表板中创建文件夹

动物文件夹的截图

这是一种更有效的组织所有照片的方式,您可以创建几个相册,例如:

 my-vacations/germany/berlin 
      road-trip/2015/route-66

URL 转换渲染

作为 Cloudinary API 的一部分,我们可以通过使用 URL 参数设置来操纵图像,就像我们在书籍页面上所做的那样:

  1. 转到http://localhost:3000/books

  2. 打开您的 Web 检查器并检查第一张图像的呈现代码;您将看到以下代码:

      <img src="http://res.cloudinary.com/n6b/image/upload
       /c_fill,e_brightness:20,h_200,q_80,r_5,w_280/v1/cities/sample01.jpg
       .png" class="materialboxed initialized" height="200" width="280"> 

API 创建img标签,并应用app/views/books.swig中定义的对象属性作为 URL 参数,如下面的代码所示:

      {{ cloudinary.image(item.image.public_id, { width: 280, height: 200,
       quality: 80,crop: 'fill',format:'png', effect: 'brightness:20',
       radius: 5, class:'materialboxed' }) | safe }} 

对象属性 URL 参数
width: 280 w_280
height: 200 h_200
crop: fill c_fill
quality: 80 q_80
effect:brightness:20 e_brightness:20
半径:5 r_5

花括号和安全过滤器{{... | safe}}Swig模板引擎的标记,用于在视图上安全地呈现变量。

此外,我们还可以直接使用img标签,如下面的代码所示:

 <img class="materialboxed" src="img/{{ item.image.url }}" height='200'
       width='100%' alt="{{ item.title }}" 
       data-caption="{{item.description}}">
      </img>

添加原始图像的直接链接

我们还可以使用 API 生成原始图像链接,而不应用任何转换:

  1. 打开app/views/books.swig并添加以下突出显示的代码:
      <div class="icon-block"> 
      <h5 class="center">{{ item.title }}</h5> 
        {{ cloudinary.image(item.image.public_id, { width: 280, height:
          200, quality: 80,crop: 'fill',format:'png', effect:
          'brightness:20', radius:5,class:'materialboxed' }) | safe }} 
       {# 
          Swig comment tag 
          <img class="materialboxed" src="img/{{ item.image.url }}"
           height='200' width='100%' alt="{{ item.title }}"
           data-caption="{{item.description}}">
          </img> 
      #} 
      <p class="light">{{ item.description }}</p> 
      <a href="{{ cloudinary.url(item.image.url) }}" target="_blank"> 
      Link to original image 
      </a> 
      </div> 

  1. 现在当我们点击链接到原始图像时,我们可以在另一个浏览器窗口中看到完整的图像:添加原始图像的直接链接

带有原始图像链接的书籍页面截图

重要的是要注意,我们还使用了Materialize.css框架中的简单colorbox,因此当我们将鼠标悬停在图像上时,我们可以看到一个图标,显示全尺寸的图像。

总结

我们已经到达了另一章的结尾。通过这一章,我们完成了一系列四章,讨论了使用 Node.js 进行软件开发的 MVC 模式。

在本章中,我们看到了如何构建一个使用云服务上传和操作图像的应用程序,还展示了如何应用效果,如亮度和边框半径。此外,我们还看到了如何使用简单的界面框架Materialize.css构建简单的图像库。

我们探索了一种不同的使用 ORM 模块的方式,并直接以 JSON 格式将所有有关图像的信息保存在 MongoDB 中。

在下一章中,我们将看到如何使用 Node 和 Firebase 云服务构建 Web 应用程序。

第五章:使用 MongoDB 地理空间查询创建门店定位器应用程序

在本章中,我们将构建一个应用程序,仅使用 express 框架、Google 地图 API 和纯 JavaScript 存储地理位置数据的坐标(纬度和经度),并在地图上显示它们。

如今,使用 JavaScript 库是非常常见的,但大多数情况下它们仅用于应用程序的前端,通常使用 JSON 格式的数据消耗端点,并使用 Ajax 更新 UI。但是我们将仅在后端使用 JavaScript,构建一个 MVC 应用程序。

此外,我们将使用 MongoDB 的一个非常强大的功能,即能够在坐标中生成索引,使用诸如$near$geometry等操作符,以定位地图中靠近特定位置的某些记录。

在本章中,我们将涵盖以下主题:

  • 在 MongoDB 中创建用于存储坐标的模型/架构

  • 创建2d球体索引

  • 处理 Google Maps API

  • 处理 HTML5 地理位置 API

  • 在模板中混合 Swig 变量和纯 JavaScript

我们正在构建什么

在本章中,我们将构建一个门店定位器应用程序和一个简单的添加门店界面。结果如下截图所示:

我们正在构建什么

主屏幕

创建基线应用程序

我们将使用与第四章中使用的express-generator相同的版本,不要拍照,创造它-为摄影师设计的应用程序。这次,我们不需要任何额外的模块来完成我们的任务:

  1. 创建一个名为chapter05的文件夹。

  2. chapter05文件夹中打开您的终端/ shell 并输入以下命令:

 yo express

提示

请注意,我们已经在第四章中安装了generator-express

  1. 现在,按照以下顺序填写问题:
  • 选择N:我们已经创建了一个文件夹

  • 选择MVC:作为应用程序类型

  • 选择Swig:作为模板引擎

  • 选择None:作为 CSS 预处理器

  • 选择MongoDb:作为数据库

  • 选择Gulp:用于 LiveReload 和其他功能

提示

如果您从未听说过Gulp,不要担心;在本书的后面,我们将看到并解释一些构建工具。

重构默认结构

正如我们所知,并且正如我们之前所做的,我们需要对我们的应用程序结构进行一些调整,以使其更具可扩展性并遵循我们的 MVC 模式:

  1. app/views文件夹中,创建一个名为pages的文件夹。

  2. app/views文件夹中,创建一个名为partials的文件夹。

  3. 将所有文件从views文件夹移动到pages文件夹。

为页脚和页眉创建部分视图

现在,作为最佳实践,让我们为页脚和页眉创建一些部分文件:

  1. app/view/partials/中创建一个名为footer.html的文件。

  2. app/view/partials/中创建一个名为head.html的文件。

将 Swig 模板设置为使用 HTML 扩展名

正如您所看到的,我们使用了.html文件扩展名,与之前的示例不同,我们使用了.swig文件扩展名。因此,我们需要更改 express app.engine配置文件,以便使用这种类型的扩展名:

  1. app/config/中打开express.js文件。

  2. 替换以下突出显示的代码行:

      app.engine('html', swig.renderFile); 
      if(env == 'development'){ 
        app.set('view cache', false); 
        swig.setDefaults({ cache: false }); 
      } 
      app.set('views', config.root + '/app/views/pages'); 
      app.set('view engine', 'html'); 

这样我们就可以在应用程序模板中使用.html文件扩展名。

创建部分文件

现在是时候创建部分文件本身了:

  1. app/views/partials中打开head.html并添加以下代码:
      <head> 
        <meta charset="UTF-8"> 
        <meta name="viewport" content="width=device-width"> 
        <title>{{ title }}</title> 
        <!--Let browser know website is optimized for mobile--> 
        <meta name="viewport" content="width=device-width, initial-scale=
           1.0"/> 
        <!-- Import Google Material font and icons --> 
        <link href="https://fonts.googleapis.com/icon?family=
          Material+Icons" rel="stylesheet"> 
        <!-- Compiled and minified CSS --> 
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax
          /libs/materialize/0.97.6/css/materialize.min.css"> 
        <link rel="stylesheet" href="/css/style.css"> 
        <!--Import jQuery before materialize.js--> 
        <script type="text/javascript" src="https://code.jquery.com/
           jquery-2.1.1.min.js"></script> 
        <!-- Compiled and minified JavaScript --> 
        <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize
          /0.97.6/js/materialize.min.js"></script> 
        <!-- Google Maps API to track location  --> 
        <scriptsrc="https://maps.googleapis.com/maps/api/js?key=<YOUR
          API KEY GOES HERE>"></script> 
      </head> 

提示

请注意,我们已经包含了一个名为materialize.cssCSS框架和 Google 地图 API 链接:

  1. app/views/partials打开footer.html并添加以下代码:
      <footer class="page-footer teal darken-1"> 
      <div class="container"> 
        <div class="row"> 
          <div class="col l6s12"> 
            <h5 class="white-text">Some Text Example</h5> 
            <p class="grey-text text-lighten-4">Lorem ipsum dolor
              sit amet, consecteturadipiscingelit, sed do 
              eiusmodtemporincididuntutlabore et dolore magna aliqua.
              Utenim ad minim veniam, quisnostrud 
              exercitationullamcolaboris nisi utaliquip ex
              eacommodoconsequat. Duisauteirure dolor in reprehenderit
              in voluptatevelitessecillumdoloreeufugiatnullapariatur.</p> 
          </div> 
          <div class="col l3s12"> 
            <h5 class="white-text">Sample Links</h5> 
            <ul> 
              <li><a class="white-text" href="#!">Link 1</a></li> 
              <li><a class="white-text" href="#!">Link 2</a></li> 
              <li><a class="white-text" href="#!">Link 3</a></li> 
              <li><a class="white-text" href="#!">Link 4</a></li> 
            </ul> 
          </div> 
          <div class="col l3s12"> 
            <h5 class="white-text">Sample Links</h5> 
            <ul> 
              <li><a class="white-text" href="#!">Link 1</a></li> 
              <li><a class="white-text" href="#!">Link 2</a></li> 
              <li><a class="white-text" href="#!">Link 3</a></li> 
              <li><a class="white-text" href="#!">Link 4</a></li> 
            </ul> 
          </div> 
        </div>
      </div> 
      <div class="footer-copyright"> 
      <div class="container"> 
        MVC Express App for: <a class="white-text text-darken-2"
        href="#">Node.js 6 Blueprints Book</a>
      </div> 
      </div> 
      </footer> 
      <!-- Live reload for development --> 
        {% if ENV_DEVELOPMENT %} 
          <scriptsrc="img/livereload.js"></script> 
        {% endif %} 
      <!--InitRsponsiveSidenav Menu  --> 
      <script> 
            (function ($) { 
              $(function () { 
                  $('.button-collapse').sideNav(); 
              }); 
            })(jQuery); 
      </script> 

创建应用程序模板文件

现在我们将替换generator创建的模板文件的内容:

  1. 打开app/views/pages/中的index.html并添加以下代码:
      {% extends 'layout.html' %} 
      {% block content %}
      <div id="map" style="height: 300px"></div> 
        <div class="section"> 
          <div class="container"> 
          <br> 
            <h1 class="header center teal-text">{{ title }}</h1> 
            <div class="row center"> 
              <h5 class="header col s12 light">Welcome to {{ title }}
              </h5> 
            </div> 
            <div class="row center"> 
              <a href="locations/add" id="download-button"
                class="btn-large waves-effect waves-light teal">
                Add your location
             </a> 
            </div> 
             <br><br> 
          </div> 
        </div> 
         <!-- Tracking current user position --> 
         <scriptsrc="img/getCurrentPosition.js"></script> 
         {% endblock %} 

提示

请注意getCurrentPosition.js文件添加到index.html模板中。在本章的后面,我们将解释这个文件发生了什么。

  1. 打开app/views/pages/中的layout.html并添加以下代码:
      <!doctype html> 
      <html lang="en"> 
      {% include "../partials/head.html" %} 
      <body> 
        <nav class="teal" role="navigation"> 
        <div class="nav-wrapper container"><a id="logo-container"
          href="/" class="brand-logo">Logo</a> 
          <ul class="right hide-on-med-and-down"> 
            <li><a href="/locations">Locations</a></li> 
            <li><a href="/locations/add">Add Location</a></li> 
            <li><a href="/stores">Stores</a></li> 
          </ul> 
          <ul id="nav-mobile" class="side-nav" style="transform:
            translateX(-100%);"> 
            <li><a href="/locations">Locations</a></li> 
            <li><a href="/locations/add">Add Location</a></li> 
            <li><a href="/stores">Stores</a></li> 
          </ul> 
          <a href="#" data-activates="nav-mobile" class="button-
           collapse"><i class="material-icons">menu</i></a> 
        </div> 
      </nav> 
      {% block content %}{% endblock %} 
       <!-- Footer --> 
       {% include "../partials/footer.html" %} 
      </body> 
      </html> 
  1. 打开app/views/pages/中的error.html并添加以下代码:
      {% extends 'layout.html' %} 
      {% block content %} 
      <div class="section"> 
        <div class="container"> 
        <br> 
          <h1 class="header center teal-text">{{ message }}</h1> 
          <div class="row center"> 
            <h3 class="header col s12 light">{{ error.status }}</h3> 
          </div> 
          <div class="row center"> 
            <pre>{{ error.stack }}</pre> 
          </div> 
          <br><br> 
        </div> 
      </div> 
      {% endblock %} 

现在我们有了开始应用程序开发所需的基线,但我们需要设置getCurrentPosition.js文件。

使用 Geolocation HTML5 API

我们可以使用各种资源来获取用户的位置,所以在这个例子中我们使用了HTML5 API。我们将使用外部 JavaScript 文件来创建一个显示用户精确位置的地图:

  1. 创建一个名为getCurrentPosition.js的文件,并将其保存在public/js文件夹中。

  2. 将以下代码放入getCurrentPosition.js中:

      function getCurrentPosition() { 
          // Check boreswer/navigator support 
      if (navigator.geolocation) { 
      var options = { 
        enableHighAccuracy : true, 
        timeout : Infinity, 
        maximumAge : 0 
      }; 
        navigator.geolocation.watchPosition(getUserPosition, trackError,
        options); 
      }
      else { 
        alert('Ops; Geolocation is not supported'); 
      } 
         // Get user position and place a icon on map 
      function getUserPosition(position) { 
            // Check longitude and latitude 
      console.log(position.coords.latitude); 
      console.log(position.coords.longitude); 
            // Create the user' coordinates 
      var googlePos = new google.maps.LatLng(position.coords.latitude,
      position.coords.longitude); 
      var mapOptions = { 
        zoom : 12,
        center :googlePos, 
        mapTypeId :google.maps.MapTypeId.ROADMAP 
      }; 
        // Set a variable to get the HTML div 
        var mapObj = document.getElementById('map'); 
        // Create the map and passing: map div and map options 
        var googleMap = new google.maps.Map(mapObj, mapOptions); 
        // Setup a marker on map with user' location 
        var markerOption = { 
          map :googleMap, 
          position :googlePos, 
          animation :google.maps.Animation.DROP 
        }; 
      // Create a instance with marker on map 
        var googleMarker = new google.maps.Marker(markerOption); 
        // Get the user's complete address information using the Geocoder
        //Google API 
        var geocoder = new google.maps.Geocoder(); 
          geocoder.geocode({ 
             'latLng' : googlePos 
          },
          function(results, status) { 
            if (status == google.maps.GeocoderStatus.OK) { 
              if (results[1]) { 
                var popOpts = { 
                content : results[1].formatted_address, 
                position :googlePos 
                }; 
                // Setup an info window with user information 
                var popup = new google.maps.InfoWindow(popOpts); 
                google.maps.event.addListener(googleMarker,
                'click', function() { 
                   popup.open(googleMap); 
                 }); 
              }
              else { 
                alert('No results found'); 
              } 
            }
            else { 
             alert('Uhh, failed: ' + status); 
            } 
          }); 
        } 
        // Setup a error function 
        function trackError(error) { 
        var err = document.getElementById('map'); 
         switch(error.code) { 
         case error.PERMISSION_DENIED: 
         err.innerHTML = "User denied Geolocation."; 
         break; 
         case error.POSITION_UNAVAILABLE: 
         err.innerHTML = "Information is unavailable."; 
         break; 
         case error.TIMEOUT: 
         err.innerHTML = "Location timed out."; 
         break; 
         case error.UNKNOWN_ERROR: 
         err.innerHTML = "An unknown error."; 
         break; 
        } 
        }
      } 
      getCurrentPosition(); 

因此,当我们转到http://localhost:3000/时,我们可以在地图上看到我们的地址指出,就像以下屏幕截图中一样:

使用 Geolocation HTML5 API

启用地理定位的主屏幕

提示

请注意,您的浏览器将请求权限以跟踪您的位置

创建应用程序控制器

现在的下一步是创建应用程序控制器:

  1. app/controllers/文件夹中创建一个名为locations.js的新文件,并添加以下代码:
      var express = require('express'), 
      router = express.Router(), 
      mongoose = require('mongoose'), 
      Location = mongoose.model('Location'); 
      module.exports = function (app) { 
      app.use('/', router); 
      }; 
      router.get('/locations', function (req, res, next) { 
      Location.find(function (err, item) { 
      if (err) return next(err); 
        res.render('locations', { 
          title: 'Locations', 
          location: item, 
          lat: -23.54312, 
          long: -46.642748 
        }); 
        //res.json(item); 
          }); 
      }); 
      router.get('/locations/add', function (req, res, next) { 
      res.render('add-location', { 
      title: 'Insert Locations' 
          }); 
      }); 
      router.post('/locations', function (req, res, next) { 
          // Fill loc object with request body 
      varloc = {
        title: req.body.title, 
        coordinates: [req.body.long, req.body.lat] 
      }; 
      var locations = new Location(loc); 
      // save the data received 
       locations.save(function(error, item) { 
       if (error) { 
         returnres.status(400).send({ 
         message: error 
         }); 
       } 
        //res.json({message: 'Success', obj: item}); 
         res.render('add-location', { 
         message: 'Upload with Success', 
         obj: item 
              }); 
          }); 
      });  

请注意,我们放置了一个固定的位置来居中地图,并创建了 3 条路线:

  • router.get('/locations',...);以从 MongoDB 获取所有位置

  • router.get('/locations/add',...);以呈现添加位置表单

  • router.post('/locations',...);以将新位置添加到 MongoDB

另一个重要的要点是get(/locations)上的注释代码:

 //res.status(200).json(stores);.

这样我们可以返回一个纯 JSON 对象,而不是使用变量渲染模板。

创建模型

现在让我们创建我们的模型来保存位置数据:

app/models文件夹中,创建一个名为locations.js的文件,并添加以下代码:

      // Example model 
      var mongoose = require('mongoose'), 
        Schema = mongoose.Schema; 
      varLocationSchema = new Schema({ 
        title: String, 
        coordinates: { 
          type: [Number], 
          index: '2dsphere' 
        },  
        created: { 
          type: Date, 
          default: Date.now 
        } 
      }); 
      mongoose.model('Location', LocationSchema);  

重要的是注意前一个代码中坐标属性的数据类型和 2dsphere 的索引。

提示

您可以在 MongoDB 的官方文档中阅读有关 2dsphere 的更多信息:docs.mongodb.com/manual/core/2dsphere/

创建视图模板

现在让我们创建view文件。这个文件对我们的应用程序非常重要,因为这是我们将Swig变量资源与我们的 JavaScript 代码集成的地方:

  1. 创建一个名为locations.html的文件,并将其保存在app/views/pages/文件夹中。

  2. 将以下代码放入locations.html文件中:

      {% extends 'layout.html' %} 
      {% block content %} 
      <div class="section"> 
        <div class="container"> 
        <br><br> 
          <h1 class="header center teal-text">{{ title }}</h1> 
          <div class="row center"> 
            <h5 class="header col s12 light">Welcome to 
              {{ title }}
            </h5> 
          </div> 
          <div class="row"> 
            <div class="col s12"> 
            <form action="/nearme" method="POST"> 
              <div class="row"> 
                <div class="col s12" id="map" style="height:600px;
                 width: 100%; margin-bottom: 20px"></div> 
                <br> 
                  <h5 class="grey-text center"> 
                            Find a store near by you 
                   </h5> 
                 <br> 
                 <div class="input-field col s5"> 
                   <input placeholder="Insert Longitude"
                    name="longitude" id="longitude" type="text"
                    class="validate" value="{{long}}">
                   <label for="longitude">Longitude</label> 
                 </div> 
                 <div class="input-field col s5"> 
                 <input placeholder="Insert latitude" name="latitude"
                   id="latitude" type="text" class="validate"
                   value="{{lat}}"> 
                 <label for="latitude">Latitude</label> 
                 </div> 
                 <div class="input-field col s2"> 
                   <select class="browser-default" name="distance"
                    id="distance">
                     <option value="" disabled selected>Distance
                     </option>
                     <option value="2">2 Km</option> 
                     <option value="3">3 km</option> 
                     <option value="9">9 km</option> 
                   </select> 
                 </div> 
               </div> 
               <div class="row"> 
               <button class="btn waves-effect waves-light"
                 type="submit" name="action">SUBMIT</button> 
               </div> 
             </form> 
             <br> 
           </div> 
         </div> 
       </div> 
       </div> 

上一个代码非常简单;我们只有一个空的map div:

 <div class="col s12" id="map" style="height: 600px; width: 100%;
        margin-bottom: 20px"></div> 

我们还有一个简单的表单,使用POST方法根据纬度和经度查找最近的位置:

 <form action="/nearme" method="POST">

创建视图模板

locations.html 的屏幕截图

接下来最重要的代码是:

  1. locations.html文件的末尾添加以下代码:
      <script type="text/javascript"> 
      var loadMap = function() { 
          // Center map with current lat and long (Simulated with fixed
            point for this example) 
         var googlePos = new google.maps.LatLng({{ lat }} , {{ long }}); 
          // Setup map options 
         var mapOptions = { 
           zoom : 12, 
           center :googlePos, 
           mapTypeId :google.maps.MapTypeId.ROADMAP 
         }; 
        // Set a variable to get the HTML div 
        var mapObj = document.getElementById('map'); 
        var googleMap = new google.maps.Map(mapObj, mapOptions); 
         // Create markers array to hold all markers on map 
        var markers = []; 
        // Using the Swig loop to get all data from location variable 
        {% for item in location %} 
            // Setup a lat long object 
          var latLng = new google.maps.LatLng({{ item.coordinates[1] }},
           {{ item.coordinates[0] }}); 
            // Create a marker 
          var marker = new google.maps.Marker({ 
            map :googleMap, 
            position: latLng, 
            animation :google.maps.Animation.DROP 
          }); 
          markers.push(marker); 
            // Setup the info window 
          varinfowindow = new google.maps.InfoWindow(); 
            // Add an event listener to click on each marker and show
               an info window 
          google.maps.event.addListener(marker, 'click', function () { 
          // using the tittle from the Swig looping 
            infowindow.setContent('<p>' + " {{ item.title }} " + '</p>'); 
            infowindow.open(googleMap, this); 
          }); 
          {% endfor %} 
        }; 
       // load the map function 
       window.onload = loadMap; 
       </script> 
       {% endblock %} 

这段代码片段做了很多事情,包括创建一个新的地图对象:

      varmapObj = document.getElementById('map'); 
      vargoogleMap = new google.maps.Map(mapObj, mapOptions); 

它还添加了来自 MongoDB 并位于位置对象循环内的标记或点:

      {% for item in location %} 
         ... 
      {% endfor %}

您可以看到上一个代码的每一行都有一个注释;这样很容易理解每一行发生了什么。

  1. 让我们创建一个新文件。创建一个名为add-location.html的文件,并将其保存在app/views/pages/文件夹中。

  2. 将以下代码放入add-location.html文件中:

      {% extends 'layout.html' %} 
      {% block content %} 
      <div class="section"> 
        <div class="container"> 
        <br><br> 
          <h1 class="header center teal-text">{{ title }}</h1> 
          <div class="row center"> 
            <h5 class="header col s12 light">Welcome to 
             {{ title }}
            </h5> 
          </div> 
          <div class="row"> 
            <div class="col s12"> 
                {% if message %} 
                  <h4 class="center teal-text"> 
                        {{ message }} 
                  </h4> 
                {% endif %} 
                <h5 class="grey-text"> 
                      Insert a new location 
                </h5> 
                <br> 
                <form action="/locations" method="POST"> 
                  <div class="row"> 
                  <div class="input-field col s4"> 
                    <input placeholder="Insert Location Title"
                     name="title" id="title" type="text" class="validate"> 
                    <label for="title">Title</label> 
                    </div> 
                    <div class="input-field col s4"> 
                      <input placeholder="Insert Longitude"
                       name="long" id="long" type="text" class="validate"> 
                      <label for="long">Longitude</label> 
                    </div>  
                    <div class="input-field col s4"> 
                    <input placeholder="Insert lat" name="lat" id="lat" 
                     type="text" class="validate"> 
                    <label for="lat">Latitude</label> 
                    </div> 
                      <br> 
                      <br> 
                    <div class="col s12 center"> 
                    <button class="btn waves-effect waves-light" 
                     type="submit" name="action">SUBMIT</button> 
                    </div> 
                  </div> 
                </form> 
                </div> 
              </div> 
            </div> 
          </div> 
          {% endblock %} 

这是一个简单的表单,用于将一些位置添加到 MongoDB,并且将看起来像以下屏幕截图:

创建视图模板

add-location.html 的屏幕截图

将位置添加到 MongoDB

现在是我们应用程序的有趣部分。我们需要在我们的应用程序中插入记录;出于教学目的,我们将使用表单(add-location.html)逐个插入记录。

该示例展示了如何插入一条记录,您应该对其他记录执行相同的操作。

提示

您可以跳过这一步,加载填充数据库的示例文件,但我们建议您按照本书中的步骤进行操作。

在本示例结束时,我们将解释如何使用 RoboMongo 面板一次加载所有记录。

  1. 在项目根文件夹打开终端/Shell,并输入以下命令:
gulp

提示

请注意,在执行上述操作之前,您必须确保您的 MongoDB 已经启动。

  1. 转到http://localhost:3000/locations/add,并填写以下信息的表单:

提示

请注意,您也需要将地图中心设置为您自己的位置,在locations.js控制器的纬度和经度属性上:

router.get('/locations', function (req, res, next) {
Location.find(function (err, item) {
...
res.render('locations', {
...
lat: -23.54312,
long: -46.642748
});
});
});

标题 = Republica

经度 = -46.642748

纬度 = -23.54312

点击提交按钮,您将在地图上方看到一个成功消息。

  1. 现在我们将使用 RoboMongo 界面添加接下来的七个位置。复制以下代码:
      db.locations.insert( 
      [{ 
          "title": "Mackenzie", 
          "coordinates": [-46.651659, -23.54807] 
      }, { 
          "title": "Shopping Maia B", 
          "coordinates": [-46.539545, -23.44375] 
      }, { 
          "title": "MorumbiSaraiva", 
          "coordinates": [-46.699053, -23.62376] 
      }, { 
          "title": "Shopping Center Norte", 
          "coordinates": [-46.617417, -23.51575] 
      }, { 
          "title": "Mooca Plaza Shopping", 
          "coordinates": [-46.594408, -23.57983] 
      }, { 
          "title": "Shopping Metro Tucuruvi", 
          "coordinates": [-46.602695, -23.47984] 
      }, { 
          "title": "Market Place", 
          "coordinates": [-46.696713, -23.61645] 
      }] 
      ) 

  1. 在 RoboMongo 界面上,选择左侧面板上的 maps-api-development 数据库。

  2. 将代码粘贴到 RoboMongo 界面中:将位置添加到 MongoDB

RoboMongo 界面终端的截图

  1. 让我们来检查结果:双击左侧菜单上的locations集合。

  2. 在 RoboMongo 视图的右侧,点击以表格模式查看结果;您将看到以下结果:将位置添加到 MongoDB

RoboMongo 面板的截图

此时,我们已经在 http://localhost:3000/locations 的地图上有了所有位置,但是附近商店的查找表单仍然无法工作,因此我们需要设置一个 MongoDB 2dsphere 索引。

了解 MongoDB 上的地理空间索引

从 MongoDB 的2.4版本开始,我们可以使用GeoJSON格式进行地理空间搜索。

提示

您可以在官方链接处找到有关 GeoJSON 的更多信息:geojson.org/

GeoJSON是一个用于格式化坐标形状的开源规范。它被广泛使用,并且非常适用于使用地理数据制作应用程序。这种格式非常简单,我们在位置模型中使用了这种格式,正如您所看到的:

var LocationSchema = new Schema({ 
  title: String, 
  coordinates: { 
    type: [Number], 
    index: '2dsphere' 
  }, 
  created: { 
    type: Date, 
    default: Date.now 
  } 
}); 

突出显示的代码是用于存储坐标的 GeoJSON 格式。

提示

您可以在这里阅读更多关于 MongoDB 上的地理空间查询:docs.mongodb.com/manual/reference/operator/query-geospatial/,以及更多地理空间索引信息:docs.mongodb.com/manual/applications/geospatial-indexes/

在 MongoDB 中创建 2dsphere 索引

让我们在 MongoDB 中检查我们的位置集合:

  1. 打开你的 RoboMongo,并在左侧面板上选择maps-api-development数据库。

  2. 双击locations集合,您将看到以下数据:在 MongoDB 中创建 2dsphere 索引

索引之前的位置集合截图

您会注意到我们只有一个带有id索引的文件夹;这是 MongoDB 的默认设置。

  1. 复制以下代码并粘贴到 RoboMongo 界面中:
db.locations.ensureIndex({ 'coordinates' : '2dsphere'})

  1. 点击右上角菜单栏中的播放按钮。

结果将如下截图所示:

在 MongoDB 中创建 2dsphere 索引

ensure.index()后的截图

请注意,现在我们已经创建了 2dsphere 索引。

检查地理位置应用

现在是测试应用程序的时候了。我们已经在我们的数据库中创建了八条记录,已经使用 ensure.index() MongoDB 对所有位置进行了索引,我们已经可以在地图中看到所有点的渲染,就像下面的截图中所看到的那样:

检查地理位置应用

locations.html 的截图

在上一个屏幕截图中,您可能会注意到地图上的点彼此之间相距较远,这能够显示当我们改变距离搜索字段时所显示的点之间的距离差异。

在这个例子中,我们可以在搜索栏中插入任何纬度和经度,但我们只是固定这个字段来说明应用程序的地理定位功能。

当我们首次访问位置路由时,我们会显示数据库中的所有记录,就像我们在上一个屏幕截图中看到的那样。

让我们改变 locations.html 表单上的距离,看看会发生什么;转到 http://localhost:3000/locations,在距离字段中选择2km,然后点击提交按钮。

在 MongoDB 中使用\(near 和\)geometry 函数进行新查询的结果将如下所示:

检查地理定位应用程序

通过 2km 筛选的位置页面的屏幕截图

这对于商店定位应用程序来说是非常有用的信息,但我们无法看到我们正在寻找的最近点在哪里。为了方便查看,我们将在地图上的左侧添加一个点列表,按从最近到最远的顺序列出。

按距离排序点

让我们添加一些代码行,使我们的搜索更直观:

  1. 在 app/views/pages/locations.html 中添加以下行,在突出显示的代码之间:
 <div class="row">      <div class="col s3"> 
              ... 
 </div> <div class="col s9"> <form action="/nearme" method="POST">           ... 
     </div> 
     </div> 

提示

请注意,您可以在 Packt Publishing 网站或本书的官方 GitHub 存储库上下载完整的代码。

  1. 在{% endfor %}循环之后,在 locations.html 的末尾添加以下函数:
      // get all the pan-to-marker class 
      var els = document.querySelectorAll(".pan-to-marker"); 
      // looping over all list elements 
      for (vari = 0, len = els.length; i<len; i++) { 
        els[i].addEventListener("click", function(e){ 
          e.preventDefault(); 
     // Use -1 for index because loop.index from swig starts on 1 
     var attr = this.getAttribute('data-marker-index') -1; 
        // get longitude and latitude of the marker 
       var latitude = markers[attr].getPosition().lat(); 
       var longitude = markers[attr].getPosition().lng(); 
        console.log(latitude, longitude ); 
          // Center map and apply zoom 
           googleMap.setCenter({lat: latitude, lng: longitude}); 
           googleMap.setZoom(18); 
           }); 
      } 

现在当我们返回到位置页面时,我们可以看到地图左侧按距离排序的点列表。请参阅下面的屏幕截图:

按距离排序点

左侧商店列表的屏幕截图

现在我们可以点击左侧面板上的任何商店。我们还可以放大地图,如下面的屏幕截图所示:

按距离排序点

选定商店的 locations.html 屏幕截图

摘要

在本章中,我们涵盖了许多与 Google Maps API 和 MongoDB 上的地理空间查询相关的内容,并使用 Node.js 和一些默认的 Express 模块构建了一个完整的商店定位器应用程序。

我们涵盖了诸如 GeoJSON 文件格式以及如何在 MongoDB 上创建地理空间索引等重要内容。

本章结束了涵盖使用不同模板引擎和技术的 MVC 设计模式的五章系列。在下一章中,我们将看到如何使用一些不同的工具来创建和测试 API,构建一个 Node.js API。

第六章:使用 Restful API 和 Loopback.io 构建客户反馈应用程序

如前所述,Node.js 生态系统有各种框架用于开发强大的 Web 应用程序。在之前的章节中,我们使用了最流行的 Express 框架。

在本章中,我们将探索另一个名为 loopback.io 的框架。该框架在很大程度上基于 Express,但它为我们提供了一些更多的功能,可以快速创建 Restful API。

它有一个命令行界面CLI),可以在不使用代码的情况下创建 API,还公开了一个用于操作 HTTP 动词的接口,一种嵌入在应用程序中的 Restful 客户端,以及其他一些优势。

我们还将看到如何使用 React.js 库在我们的应用程序前端消耗此 API。

在本章中,我们将涵盖以下主题:

  • 安装 LoopBack 框架

  • LoopBack CLI 的基础知识

  • 使用命令行创建模型

  • 处理数据源和数据库关系

  • 创建一个简单的 React.js 应用程序来消耗 API

我们正在构建什么

在本章中,我们将构建一个 API 来存储任何类型的产品,例如经典的摩托车模型,并存储用户对该摩托车的评论/反馈。结果将看起来像以下屏幕截图:

我们正在构建什么

主页的屏幕截图

创建基线结构

首先让我们安装 LoopBack 框架:

  1. 打开您的终端/Shell 并键入以下命令:
npm install strongloop -g

  1. 打开您的终端/Shell 并键入以下命令:
slc loopback

  1. 输入名称:目录选项为 chapter-06。

  2. 选择 empty-server(一个没有任何内容的 LoopBack API)

配置模型或数据源)选项。

不要担心输出的结尾,我们将在下一个主题中解释这一点。

结果将是以下文件夹和文件的结构:

创建基线结构

文件夹和文件的屏幕截图

结构非常简单;几乎所有 LoopBack 的配置都在 JSON 文件中,如component-config.jsonconfig.jsondatasources.json,以及server文件夹中的所有其他文件。

提示

您可以通过在终端窗口中键入以下命令来了解有关slc命令行的更多信息:slc -help。

使用命令行创建模型

此时,我们已经有了开始开发 API 所需的结构。

现在我们将使用命令行来创建应用程序的模型。我们将构建两个模型:一个用于产品/摩托车,另一个用于用户/消费者。

  1. 在 chapter-06 文件夹中打开终端/Shell 并键入以下命令:
slc loopback:model

  1. 填写摩托车模型的以下信息,如下图所示:使用命令行创建模型

创建摩托车模型后的终端输出的屏幕截图

  1. 填写属性名称:
      Property name: image
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

      Property name: make
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

      Property name: description
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

      Property name: model
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

      Property name: category
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

      Property name: year
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:
  1. 让我们创建客户模型。打开终端/Shell 并键入以下命令:
slc loopback:model

  1. 填写审查模型的信息,如下图所示:使用命令行创建模型

创建模型审查后的终端输出的屏幕截图

  1. 填写属性名称:
      Property name: name
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

      Property name: email
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

      Property name: review
      ? Property type: string
      ? Required? Yes
      ? Default value[leave blank for none]:

即使使用命令行,我们也可以检查和编辑刚刚创建的模型。

提示

这里需要注意的一个重要点是,common 属性创建一个目录并与 client 和 server 文件夹共享。如果使用 server 属性,代码将存储在 server 文件夹中,并且不与 client 文件夹共享。

使用命令行创建模型后编辑模型

我们可以直接在 common/models/文件夹中编辑模型。我们为每个创建的模型有两个文件。

第一个是一个带有所有属性的 JSON 文件,如我们在 review.json 文件中所见的代码:

    { 
      "name": "review", 
      "base": "PersistedModel", 
      "idInjection": true, 
      "options": { 
        "validateUpsert": true 
      }, 
      "properties": { 
        "name": { 
        "type": "string", 
        "required": true 
      }, 
      "email": { 
        "type": "string", 
        "required": true 
      }, 
      "review": { 
          "type": "string", 
          "required": true 
      } 
    }, 
      "validations": [], 
      "relations": {}, 
      "acls": [], 
      "methods": {} 
    } 

第二个是一个 JavaScript 文件,如我们在 review.js 文件中所见的代码:

    module.exports = function(Review) { 
    }; 

JavaScript 文件是您可以配置应用程序方法的地方。您可能会注意到,在创建模型时,其功能是空的;这是因为 LoopBack 框架通过使用 Express 框架来抽象常见的 CRUD 操作,这与我们在上一章中所做的操作相同。

通过命令行创建数据源

我们将使用数据库存储客户的反馈,因此我们将使用 LoopBack CLI 创建数据源:

  1. 在根项目中打开终端/ shell 并输入以下命令:
slc loopback:datasource

  1. 使用以下信息填写选项:通过命令行创建数据源

数据源终端输出的屏幕截图

请注意,最终选项是安装 MongoDB 连接器。因此,请不要忘记在 MongoDB 实例上创建数据库:motorcycle-feedback。

提示

在本书示例中,我们不使用数据库的用户名和密码,但强烈建议您在生产环境中使用用户名和强密码。

数据源配置可以在 server/datasources.json 文件中找到,如下代码所示:

    { 
      "motorcycleDataSource": { 
        "host": "localhost", 
        "port": 27017, 
        "database": "motorcycle-feedback", 
        "password": "", 
        "name": "motorcycleDataSource", 
        "user": "", 
        "connector": "mongodb" 
      } 
    } 

Loopback API 为我们提供了在不同数据库上配置数据源的可能性。

将模型连接到数据源

下一步是建立模型和数据源之间的关系,为此我们将手动编辑文件。

请记住,命令行也提供了此功能,使用 slc loopback:relation:,但是在撰写本文时,生成器中存在错误,我们目前无法使用此功能。但是,这并不妨碍我们继续进行应用程序开发,因为命令行工具并非强制使用:

打开 server/model-config.json 并添加以下突出显示的代码:

      { 
        "_meta": { 
          "sources": [ 
            "loopback/common/models", 
            "loopback/server/models", 
            "../common/models", 
            "./models" 
          ], 
          "mixins": [ 
            "loopback/common/mixins", 
            "loopback/server/mixins", 
            "../common/mixins", 
            "./mixins" 
          ] 
        }, 
        "motorcycle": { 
          "dataSource": "motorcycleDataSource", 
          "public": true 
        }, 
        "review": { 
          "dataSource": "motorcycleDataSource", 
          "public": true 
        } 
      } 

在这个阶段,通常会使用称为 ARC 工具的可视界面来构建、部署和管理我们的 Node API,但是对于本书的示例,我们不会使用它,因此将所有注意力都集中在代码上。

提示

您可以在此链接找到有关 ARC 的更多信息:docs.strongloop.com/display/APIS/Using+Arc

使用 API Explorer

LoopBack API Explorer 最好的功能之一是生成一个本地主机 API 端点,允许我们查看和测试 API 生成的所有端点。

此外,它可能值得作为文档,包含所有必要的指令,如 HTTP 动词 GET、POST、UPDATE、DELETE,如果需要发送令牌访问,数据类型和 JSON 格式。

  1. 打开终端/ shell 并输入以下命令:
npm start 

  1. 转到 http://localhost:3000/explorer/#/。结果将是以下屏幕截图:使用 API Explorer

API Explorer 的屏幕截图

可以看到 API 基本 URL 和 API 版本,我们的项目名称和应用程序端点。

  1. 当我们点击review模型时,我们可以看到所有带有 HTTP 动词的端点,如下图所示:使用 API Explorer

评论端点和 HTTP 动词的屏幕截图

创建的端点如下:

当然,您也可以直接使用浏览器访问它们。

重要的是要注意 GET 和 POST 端点是相同的,区别在于:当我们想要检索内容时,我们使用 GET 方法,当我们想要插入内容时,我们使用 POST 方法,PUT 和 DELETE 也是一样,我们需要在 URL 的末尾传递 ID,如 http://localhost:3000/api/reviews/23214。

我们还可以看到每个端点右侧有一个简要描述其目的的描述。

它还具有一些其他非常有用的端点,如下图所示:

使用 API Explorer

评论端点的附加方法的屏幕截图

使用端点插入记录

现在我们将使用 API Explorer 界面向数据库中插入一条记录。我们将插入一个产品,即我们的摩托车:

  1. 转到 http://localhost:3000/explorer/#!/motorcycle/motorcycle_create。

  2. 将以下内容放入数据值字段中,然后点击“尝试一下”按钮:

      { 
         "make": "Harley Davidson", 
         "image": "images/heritage.jpg", 
         "model": "Heritage Softail", 
         "description": "An Evolution V-twin Engine!", 
         "category": "Cruiser", 
         "year": "1986" 
      } 

响应主体将如下截图所示:

使用端点插入记录

POST 成功的屏幕截图

请注意,我们有一个 HTTP 状态码200和一个新创建数据的 ID。

使用端点检索记录

现在我们将使用 API Explorer 界面从数据库中检索记录。我们将使用摩托车端点:

  1. 转到 http://localhost:3000/explorer/#!/motorcycle/motorcycle_find。

  2. 单击“尝试一下”按钮,我们将得到与之前截图相同的结果。

请注意,我们正在使用 API 资源管理器,但我们所有的 API 端点都通过 http://localhost:3000/api/公开。

  1. 转到 http://localhost:3000/api/motorcycles,您可以在浏览器上看到以下结果:使用端点检索记录

摩托车端点的屏幕截图

提示

请注意,我们正在使用一个名为JSON VIEW的 Chrome 扩展程序,您可以在这里获取:chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc

在处理大型 JSON 文件时非常有用。

添加数据库关系

现在我们已经配置了端点,我们需要在应用程序模型之间创建关系。

我们的反馈将被插入到特定类型的产品中,例如我们的摩托车示例,然后每个摩托车型号都可以接收各种反馈。让我们看看如何通过直接编辑源代码来创建模型之间的关系有多简单:

  1. 打开 common/models/motorcycle.json 并添加以下突出显示的代码:
      { 
          "name": "motorcycle", 
          "base": "PersistedModel", 
          "idInjection": true, 
          "options": { 
              "validateUpsert": true 
          }, 
          "properties": { 
            "image": { 
               "type": "string", 
               "required": true 
            }, 
            "make": { 
              "type": "string", 
              "required": true 
            }, 
            "description": { 
               "type": "string", 
               "required": true 
            }, 
            "model": { 
              "type": "string", 
              "required": true 
            }, 
            "category": { 
              "type": "string", 
              "required": true 
            }, 
            "year": { 
              "type": "string", 
              "required": true 
            } 
          }, 
          "validations": [], 
          "relations": { 
              "review": { 
                "type": "hasMany", 
                "model": "review", 
                "foreignKey": "ObjectId"
 } 
            }, 
            "acls": [], 
            "methods": {} 
      }

  1. 重新启动应用程序,打开终端窗口,然后输入以下命令:
npm start

  1. 转到 http://localhost:3000/explorer。

我们可以看到 LoopBack 已经为这种关系创建了新的端点,如下图所示:

添加数据库关系

新端点创建的屏幕截图

现在我们可以使用以下方式获取与摩托车模型相关的所有反馈:

http://localhost:3000/api/motorcycles//review。

我们还可以通过简单地将评论 ID 添加到以下 URL 中来获取一个评论:

http://localhost:3000/api/motorcycles//review/

处理 LoopBack 引导文件

在使用 LoopBack 框架的应用程序中,引导文件非常重要。这些文件在应用程序执行时启动,并可以执行各种任务。

该应用程序已经具备了所有需要的端点。因此,让我们看看如何创建一个引导文件,并使用 LoopBack 框架的另一个功能来将我们的模型迁移到数据库。

在这个例子中,我们将看到如何使用 automigrate 函数在启动应用程序时向数据库中插入一些内容:

提示

您可以在apidocs.strongloop.com/上阅读更多关于 LoopBack API 的信息。

在 server/boot 中,创建一个名为 create-sample-models.js 的新文件,并将以下内容放入其中:

      module.exports = function(app) { 
          // automigrate for models, every time the app will running,
           db will be replaced with this data. 
         app.dataSources.motorcycleDataSource.automigrate('motorcycle',
          function(err) { 
          if (err) throw err; 
          // Simple function to create content 
            app.models.Motorcycle.create( 
              [ 
                { 
                  "make": "Harley Davidson", 
                  "image": "images/heritage.jpg", 
                  "model": "Heritage Softail", 
                  "description": "An Evolution V-twin Engine!", 
                  "category": "Cruiser", 
                  "year": "1986", 
                  "id": "57337088fabe969f2dd4078e" 
                } 
              ], function(err, motorcycles) { 
                  if (err) throw err; 
                 // Show a success msg on terminal 
                   console.log('Created Motorcycle Model: \n',
                    motorcycles); 
                  }); 
                }); 
                app.dataSources.motorcycleDataSource.automigrate
                 ('review', function(err) { 
                if (err) throw err; 
                // Simple function to create content 
                app.models.Review.create( 
                  [ 
                    { 
                      "name": "Jax Teller", 
                      "email": "jax@soa.com", 
                      "id": "57337b82e630a9152ed6554d", 
                      "review": "I love the Engine and sound", 
                      "ObjectId": "57337088fabe969f2dd4078e" 
                    }, 
                    { 
                      "name": "Filip Chibs Telford", 
                      "email": "chibs@soa.com", 
                      "review": "Emblematic motorcycle of the world", 
                      "id": "5733845b00f4a48b2edd54cd", 
                      "ObjectId": "57337088fabe969f2dd4078e" 
                    }, 
                    { 
                      "name": "Clay Morrow", 
                      "email": "clay@soa.com", 
                      "review": "A classic for the eighties, i love
                        the engine sound", 
                      "id": "5733845b00f4a48b2edd54ef", 
                      "ObjectId": "57337088fabe969f2dd4078e" 
                    } 
                  ], function(err, reviews) { 
                  if (err) throw err; 
                  // Show a success msg on terminal 
                   console.log('Created Review Model: \n', reviews); 
                  }); 
                }); 
              };  

上面的代码非常简单;我们只是使用模型的对象属性创建对象。现在,每次应用程序启动时,我们都会向数据库发送一条摩托车记录和三条摩托车反馈。

这一步完成了我们的 API。尽管这是一个非常琐碎的例子,但我们探索了 LoopBack 框架的几个强大功能。

此外,我们还可以使用 ARC 编辑器。正如前面提到的,只需使用图形界面就可以创建模型和迁移。它还非常有用,比如部署和其他用途。

使用 API

现在我们将探讨如何使用此 API。我们已经看到 API 包含在:localhost:3000/api/,我们的根路径只有一些关于 API 的信息,我们可以通过访问 localhost:3000 来查看:

{
 started: "2016-05-15T15:20:24.779Z",
 uptime: 7.017
}

让我们更改 root.js 和 middleware.json 文件,并使用一些客户端库与 API 进行交互。

将 HTML 内容添加到客户端

  1. 将 server/boot 中的 root.js 文件更改为 _root.js。

  2. 打开 server/文件夹中的 middleware.json,并添加以下突出显示的代码:

      { 
        "initial:before": { 
        "loopback#favicon": {} 
      }, 
        "initial": { 
          ... 
          }, 
          "helmet#xssFilter": {}, 
          "helmet#frameguard": { 
           ... 
          }, 
           "helmet#hsts": { 
            ... 
           }, 
           "helmet#hidePoweredBy": {}, 
          "helmet#ieNoOpen": {}, 
          "helmet#noSniff": {}, 
          "helmet#noCache": { 
            ... 
           } 
        }, 
        "session": {}, 
        "auth": {}, 
        "parse": {}, 
        "routes": { 
         ... 
          } 
        }, 
        "files": { 
           "loopback#static": { 
              "params": "$!../client" 
           } 
        }, 
        "final": { 
          "loopback#urlNotFound": {} 
        }, 
         "final:after": { 
            "loopback#errorHandler": {} 
        } 
      } 

  1. 在 client 文件夹中创建一个名为 index.html 的新文件,并将其保存在 client 文件夹中。

现在我们配置应用程序以映射客户端文件夹并使其公开访问。这与我们为 Express 框架设置静态路由时非常相似。我们可以以其他方式设置应用程序的路由,但在此示例中,让我们保持这种方式。

添加 Bootstrap 框架和 React 库

现在让我们将依赖项添加到我们的 HTML 文件中;我们将使用 Bootstrap 和 React.js。

请注意,突出显示的文件是从内容传送网络CDN)提供的,但如果您愿意,您可以将这些文件存储在 client 文件夹或用于 CSS 和 JavaScript 的子目录中:

  1. 打开新创建的 index.html 文件,并添加以下代码:
      <!DOCTYPE html> 
      <html> 
      <head><title>Motorcycle Customer feedback</title></head> 
      <link rel='stylesheet' href='https://cdnjs.cloudflare.com/
       ajax/lib/twitter-bootstrap/4.0.0-alpha/css/bootstrap.min.css'> 
      <style> 
        body { 
          padding-top: 5rem; 
        } 
        .starter-template { 
          padding: 3rem 1.5rem; 
          text-align: center; 
        } 
      </style> 
        <body> 
          <nav class="navbar navbar-fixed-top navbar-dark bg-inverse"> 
          <div class="container"> 
            <a class="navbar-brand" href="#">Custumer Feedback</a> 
            <ul class="nav navbar-nav"> 
              <li class="nav-item active"> 
                <a class="nav-link" href="#">Home <span class="sr-only">
                 (current)</span></a> 
              </li> 
            </ul> 
          </div> 
          </nav> 
          <div class="container"> 
            <!-- This element's contents will be replaced with 
              your component. --> 
          <div id="title"> 
            <div class="starter-template"> 
              <h1>Motorcycle Feedback</h1> 
              <p class="lead">Add your comments about this model.</p> 
            </div> 
          </div> 
          <div class="row"> 
            <div class="col-lg-4"> 
              <div id="motorcycle"></div> 
            </div> 
            <div class="col-lg-8"> 
              <div id="content"></div> 
            </div> 
          </div> 
        </div> 
          <!-- Scripts at bottom --> 
          <script src='https://cdnjs.cloudflare.com/ajax/libs
            /jquery/2.2.1/jquery.min.js'></script> 
          <script src='https://cdnjs.cloudflare.com/ajax/libs
           /twitter-bootstrap/4.0.0-alpha/js/bootstrap.min.js'></script> 
          <script src="https://cdnjs.cloudflare.com/ajax/libs/
           babel-core/5.8.24/browser.js"></script> 
          <script src="https://cdnjs.cloudflare.com/ajax/libs
            /react/15.0.1/react.js"></script> 
          <script src="https://cdnjs.cloudflare.com/ajax/libs/react
            /15.0.1/react-dom.js"></script> 
          <script type="text/babel" src="img/reviews.js"> </script> 
          <script type="text/babel" src="img/motorcycles.js"> </script> 
      </body> 
      </html> 

如您所见,在上一个代码中,我们添加了两个文件,类型为 script text/babel。这些文件将是我们使用 React.js 库构建的应用程序组件。

提示

您可以在这里找到有关 React.js 的更多信息:facebook.github.io/react/

  1. 在 client 文件夹中,创建一个名为 images 的新文件夹。

您可以将摩托车示例图像复制并粘贴到此文件夹中。此外,您可以在 Packt Publishing 网站和书籍的官方 GitHub 存储库中下载所有示例代码。

创建 React 组件

类似于 jQuery 小部件和 AgularJS 指令,有 React.js,这是一个非常有用的库,用于创建界面组件。但是,它不像 AngularJS 或 Ember.js 那样是一个完整的框架。

思考 React.js 的方式是通过思考界面组件:一切都是一个组件,一个组件可能由一个或多个组件组成。

请参阅以下图:

创建 React 组件

模拟 React.js 组件的屏幕截图

让我们逐个创建组件,以便更好地理解:

  1. 在 client 文件夹中,创建一个名为 js 的新文件夹。

  2. 在 js 文件夹中,创建一个名为 review.js 的新文件,并添加以下内容:

      var Review = React.createClass({ 

               render: function() { 
                     return ( 
                         <div className="list-group-item"> 
                           <small className="text-muted pull-right">
                              {this.props.email}
                           </small> 
                           <h4 className="list-group-item-heading"> 
                                 {this.props.name} 
                           </h4> 
                           <p className="list-group-item-text">
                             {this.props.review}
                           </p> 
                         </div> 
                     ); 
                 } 
               });

这是列表项组件。

  1. 现在让我们添加 ReviewBox。在上一个代码之后添加以下代码:
     var ReviewBox = React.createClass({ 
           loadReviewsFromServer: function() { 
                 $.ajax({ 
                       url: this.props.api, 
                       type: 'GET', 
                       dataType: 'json', 
                       cache: false, 
                       success: function(data) { 
                             console.log(data); 
                             this.setState({data: data}); 
                       }.bind(this), 
                       error: function(xhr, status, err) { 
                             console.error(this.props.api, status,
                               err.toString()); 
                       }.bind(this) 
                 }); 
           }, 
           handleReviewSubmit: function(review) { 
                 var reviews = this.state.data; 
                 // Don' use Date.now() on production, this is here
                    just for the example. 
                 review.id = Date.now().toString(); 
                 var newReviews = reviews.concat([review]); 
                  this.setState({data: newReviews}); 
                 console.log(review); 
                 $.ajax({ 
                       url: this.props.api, 
                       dataType: 'json', 
                       type: 'POST', 
                      data: review, 
                       success: function(data) { 
                             console.log(data); 
                       }.bind(this), 
                       error: function(xhr, status, err) { 
                             this.setState({data: reviews}); 
                             console.error(this.props.api, status,
                               err.toString()); 
                       }.bind(this) 
                 }); 
           }, 
           getInitialState: function() { 
                 return { 
                       data: [] 
                 }; 
           }, 
           componentDidMount: function() { 
                 this.loadReviewsFromServer(); 
           }, 
           render: function() { 
                 return ( 
                       <div> 
                             <ReviewList data={this.state.data} /> 
                             <ReviewForm onReviewSubmit=
                              {this.handleReviewSubmit} /> 
                       </div> 
                  ); 
          } 
     });

这是 ReviewBox 组件及其两个接收组件;一个是 ReviewList 组件,另一个是 ReviewForm 组件。请注意,我们使用 jQuery 的$.get()函数从 localhost:3000/api/reviews 获取评论,使用 GET 方法。

此外,我们有一个名为 handleReviewSubmit()的函数,用于处理表单提交操作到相同的端点:localhost:3000/api/reviews,使用 POST 方法。

我们有 getInitialState()函数来设置一个数据数组,它在 componentDidMount()函数上等待一个 promise 函数:

  1. 现在让我们将 ReviewList 组件添加到 reviews.js 中。在上一个代码之后添加以下代码:
      var ReviewList = React.createClass({ 
             render: function() { 
               var reviewNodes = this.props.data.map(function(review)
               { 
                 return ( 
                     <Review name={review.name} review={review.review}
                       email={review.email} key={review.id}> </Review> 
                 ); 
               }); 
               return ( 
                   <div className="list-group"> 
                           {reviewNodes} 
                   </div> 
               ); 
             } 
      });

  1. 现在我们添加 ReviewForm 组件。在上一个代码之后添加以下代码:
      var ReviewForm = React.createClass({ 
          getInitialState: function() { 
               return {name: '', email: '', review: '', model: ''}; 
          }, 
          handleAuthorChange: function(e) { 
              this.setState({name: e.target.value}); 
          }, 
          handleEmailChange: function(e) { 
               this.setState({email: e.target.value}); 
          }, 
          handleTextChange: function(e) { 
               this.setState({review: e.target.value}); 
          }, 
          handleSubmit: function(e) { 
               e.preventDefault(); 
               var name = this.state.name.trim(); 
               var email = this.state.email.trim(); 
               var review = this.state.review.trim(); 
               var model = '57337088fabe969f2dd4078e';
                if (!review || !name) { 
                    return; 
                }
                this.props.onReviewSubmit({name: name, email:email,
                  model:model, review: review}); 
                this.setState({name: '', email: '', review: '',
                  model: ''}); 
           }, 
           render: function() { 
               return ( 
                 <div> 
                   <hr/> 
                     <form onSubmit={this.handleSubmit}> 
                       <div className="row"> 
                         <div className="col-lg-6"> 
                           <fieldset className="form-group"> 
                             <label for="InputName">Name</label> 
                             <input type="review" className=
                               "form-control" id="InputName"
                                placeholder="Name" value=
                                {this.state.name} 
                              onChange={this.handleAuthorChange} /> 
                            </fieldset> 
                          </div> 
                          <div className="col-lg-6"> 
                            <fieldset className="form-group"> 
                              <label for="InputEmail">Email</label> 
                              <input type="review" className="form-control"
                                id="InputEmail" placeholder="Email" value=
                                {this.state.email} 
                              onChange={this.handleEmailChange}/> 
                            </fieldset> 
                          </div> 
                        </div> 
                        <fieldset className="form-group"> 
                        <label for="TextareaFeedback">Feedback</label> 
                        <textarea className="form-control"
                         id="TextareaFeedback" rows="3" value=
                         {this.state.review} onChange=
                         {this.handleTextChange} /> 
                        </fieldset> 

                        <button type="submit" className=
                          "btn btn-primary" value="Post">
                             Submit
                        </button> 
                     </form> 
                 </div> 
                 ); 
            } 
      });

  1. 最后,我们只需要创建一个 React 方法来呈现所有内容。在上一个代码之后添加以下代码:
      ReactDOM.render( 
         <ReviewBox api="/api/reviews"/>,
           document.getElementById('content') 
      ); 

此前的代码片段将在

中呈现 ReviewBox 组件;简要类比 CSS 类,我们有以下组件结构:

  • ReviewBox

  • ReviewList

  • 回顾

  • ReviewForm

因此,ReviewBox 组件的 render()方法呈现两个组件:

      render: function() { 
         return ( 
            <div> 
              <ReviewList data={this.state.data} /> 
              <ReviewForm onCommentSubmit={this.handleReviewSubmit} /> 
            </div> 
        ); 
      } 

现在我们对摩托车组件做同样的操作:

  1. 在 common/js 文件夹中创建一个名为 motorcycle.js 的新文件,并添加以下代码:
      // create a interface component for motorcycle item 
      var Motorcycle = React.createClass({ 
        render: function() { 
            return ( 
              <div className="card"> 
                <img className="card-img-top" src={this.props.image}
                  alt={this.props.make} width="100%"/> 
                <div className="card-block"> 
                  <h4 className="card-title">{this.props.make}</h4> 
                  <p className="card-text">{this.props.description}</p> 
                </div> 
                <ul className="list-group list-group-flush"> 
                  <li className="list-group-item"><strong>Model:
                    </strong> {this.props.model}</li> 
                  <li className="list-group-item"><strong>Category:
                    </strong> {this.props.category}</li> 
                  <li className="list-group-item"><strong>Year:
                    </strong> {this.props.year}</li> 
                </ul> 
              </div> 
            ); 
        } 
      });

  1. 让我们添加 MotorcycleBox 组件。在上一行之后添加以下代码:
      // create a motorcycle box component 
      var MotorcycleBox = React.createClass({ 
         loadMotorcyclesFromServer: function() { 
             $.ajax({ 
               url: this.props.api, 
               type: 'GET', 
               dataType: 'json', 
               cache: false, 
               success: function(data) { 
                 console.log(data); 
                 this.setState({data: data}); 
               }
               .bind(this), 
               error: function(xhr, status, err) { 
                 console.error(this.props.api, status,
                 err.toString()); 
               }
               .bind(this) 
             }); 
         }, 
         getInitialState: function() { 
             return { 
               data: [] 
             }; 
         }, 
         componentDidMount: function() { 
             this.loadMotorcyclesFromServer(); 
         }, 
         render: function() { 
           return ( 
             <div> 
              <MotorcycleList data={this.state.data} /> 
            </div> 
          ); 
        }
      });

  1. 让我们创建一个 motorcycleList 组件。在上一行之后添加以下代码:
      // create a motorcycle list component 
      var MotorcycleList = React.createClass({ 
        render: function() { 
          var motorcycleNodes = this.props.data.map(function(motorcycle)
          { 
            console.log(motorcycle); 
            return ( 
              <Motorcycle image={motorcycle.image} make=
                {motorcycle.make} model={motorcycle.model} description=
                {motorcycle.description} category={motorcycle.category}
                year={motorcycle.year} key={motorcycle.id}>
              </Motorcycle> 
            ); 
          }); 
          return ( 
            <div className="motorcycles"> 
              {motorcycleNodes} 
            </div> 
          ); 
        }
      }); 

请注意,我们创建了一个列表来渲染数据库中的所有摩托车型号。如果您想要在此集合中添加或渲染更多项目,这是推荐的做法。对于我们的示例,我们只有一个。

最后的方法是 render()函数来渲染 MotorcycleBox 组件

  1. 在上一行之后添加以下行:
      ReactDOM.render( 
         <MotorcycleBox api="/api/motorcycles"/>, 
           document.getElementById('motorcycle') 
      ); 

此渲染方法告诉在 HTML 摩托车 div 标签内渲染 MotorcycleBox 组件:

创建新的反馈

现在是时候使用我们构建的应用程序创建新的反馈了:

  1. 打开终端/Shell 并输入以下命令:
npm start

  1. 转到 http://localhost:3000/,填写以下数据并点击提交按钮:
  • 姓名:约翰·多

  • 电子邮件:john@doe.com

  • 反馈:很棒的红白经典摩托车!

结果会立即显示在屏幕上,如下截图所示。

创建新的反馈

新创建的反馈的屏幕截图

简单检查端点

让我们对我们的 API 进行简单的检查。前面的图像显示了特定型号摩托车的四条反馈;我们可以看到在图像中出现了评论的计数,但我们的 API 有一个端点显示这些数据。

转到 http://localhost:3000/api/reviews/count,我们可以看到以下结果:

      { 
         count: 4 
      } 

禁用远程 LoopBack 端点

默认情况下,LoopBack 创建了许多额外的端点,而不仅仅是传统的 CRUD 操作。我们之前看到了这一点,包括前面的例子。但有时,我们不需要通过 API 资源公开所有端点。

让我们看看如何使用几行代码来减少端点的数量:

  1. 打开 common/models/review.js 并添加以下突出显示的代码行:
      module.exports = function(Review) { 
         // Disable endpoint / methods 
         Review.disableRemoteMethod("count", true); 
         Review.disableRemoteMethod("exists", true); 
         Review.disableRemoteMethod("findOne", true); 
         Review.disableRemoteMethod('createChangeStream', true); 
         Review.disableRemoteMethod("updateAll", true); 
      }; 

  1. 重新启动应用程序,打开您的终端/Shell,并输入以下命令:
npm start

  1. 转到 http://localhost:3000/explorer/,点击review模型。

结果将如下图所示,只有 CRUD 端点:

禁用远程 LoopBack 端点

评论端点的屏幕截图

提示

您可以在以下链接找到有关隐藏和显示端点的更多信息:docs.strongloop.com/display/public/LB/Exposing+models+over+REST#ExposingmodelsoverREST-Hidingendpointsforrelatedmodels

摘要

在本章中,我们讨论了使用 LoopBack 框架创建健壮 API 的过程,并涉及了关于 Web 应用作为数据库、模型之间关系和数据源的一些非常重要的点。

我们还看到了 Express 和 Loopback 之间的一些相似之处,并学会了如何使用 API 资源的 Web 界面。

我们使用 React.js 库构建了一个交互式界面,并接近了 React.js 的主要概念,即组件的创建。

在下一章中,我们将看到如何使用 Node.js 的一些非常有用的资源构建实时应用程序。

第七章:使用 Socket.io 构建实时聊天应用程序

一段时间以前,当 Node.js 应用程序出现时,我们有了许多新的可能性,使用 Node.js 资源和诸如 Socket.io 之类的东西来构建实时应用程序(正如该网站所说,Socket.io 实现了基于事件的双向实时通信。它在每个平台、设备或浏览器上都能工作,同时注重可靠性和速度)。

Socket.io 允许我们在客户端和服务器之间发出事件,以及其他可能性。

在本章中,我们将涵盖以下主题:

  • 安装 Socket.io

  • Socket.io 事件的基础知识

  • 创建一个 Express 聊天应用程序

  • 处理客户端的 jQuery

  • 如何在开发中使用 Gulp.js 和 LiveReload 插件

我们正在构建什么

在本章中,我们将使用 Node.js、Socket.io 和 jQuery 构建一个实时聊天应用程序:

我们正在构建什么

主屏幕的屏幕截图

从 package.json 文件开始

在本章中,我们采用了一种不同的方式来启动我们的应用程序;正如我们在之前的章节中看到的,让我们逐步构建一个 Node.js 应用程序,而不使用命令行。

让我们创建带有应用程序依赖项的 package.json 文件:

  1. 创建一个名为 chapter-07 的文件夹。

  2. 在 chapter-07 中创建一个名为 package.json 的文件,并添加以下代码:

      {
        "name": "chapter-07", 
        "description": "Build a real time chat application with
          Node.js and Socket.io", 
        "version": "0.0.1", 
        "private": true, 
        "scripts": { 
          "start": "node app.js" 
        }, 
        "dependencies": { 
          "body-parser": "¹.13.3", 
          "cookie-parser": "¹.3.3", 
          "ejs": "².3.1", 
          "express": "⁴.13.3", 
          "morgan": "¹.6.1", 
          "serve-favicon": "².3.0", 
          "socket.io": "¹.4.6" 
        }, 
        "devDependencies": { 
          "gulp": "³.9.0", 
          "gulp-nodemon": "².0.2", 
          "gulp-livereload": "³.8.0", 
          "gulp-plumber": "¹.0.0" 
        } 
      }

请注意,我们正在使用与 Express 框架推荐的模块依赖相同的模块依赖。此外,我们添加了名为 Gulp.js 的任务运行器。在本章的后面,我们将更多地解释有关Gulp的内容。

  1. 打开终端/Shell 并输入以下命令:
npm install

  1. 创建一个名为 public 的文件夹。

  2. 创建一个名为 routes 的文件夹。

  3. 创建一个名为 views 的文件夹。

在这个阶段,你的文件夹必须具有以下结构:

chapter-01
   node_modules
   public
   routes
   views
   package.json

添加配置文件

点文件在所有 Web 应用程序中都很常见;这些文件负责各种任务,包括版本控制和文本编辑器配置的配置,以及许多其他任务。

让我们为 Bower 包管理器添加我们的第一个配置文件(更多信息:bower.io/):

  1. 创建一个名为.bowerrc 的文件,并添加以下代码:
      { 
        "directory": "public/components", 
        "json": "bower.json" 
      } 

这个文件告诉 Bower 在 public/components 中安装所有应用程序组件;否则,它们将被安装在根应用程序文件夹中。

  1. 创建一个名为.editorconfig 的文件,并添加以下代码:
      # http://editorconfig.org 
      root = true 
      [*] 
      indent_style = tab 
      indent_size = 4 
      end_of_line = lf 
      charset = utf-8 
      trim_trailing_whitespace = true 
      insert_final_newline = true

这个文件标准化了整个应用程序的代码缩进类型。有许多编辑器支持这个文件,并为每个应用程序应用它们的定义。

下一个配置文件是 gitignore。正如它的名字所示,它用于告诉版本控制应忽略哪些应用程序文件。

  1. 创建一个名为.gitignore 的文件,并添加以下代码:
      node_modules/ 
      public/components 
      .sass-cache 
      npm-debug.log 

添加任务管理器文件

任务管理器在我们的应用程序中执行特定的任务。在第九章中,使用 Node.js 和 NPM 构建前端流程,我们将深入探讨它们在 Node.js 应用程序中的利用,但现在我们专注于文件本身:

  1. 创建一个名为 bower.json 的文件,并添加以下代码行:
      { 
        "name": "chapter-07", 
        "version": "0.0.1", 
        "ignore": [ 
          "**/.*", 
          "node_modules", 
          "components" 
        ] 
      } 

这是非常简单的代码,但这个文件和服务器端的 package.json 一样重要。Bower 将是前端任务管理器。在本章中,我们将看到如何使用它。接下来是 Gulp 文件。

提示

您可以在官方网站上找到有关 Gulp 文件的更多信息:gulpjs.com/

  1. 创建一个名为 gulpfile.js 的文件,并添加以下代码(代码已经完全注释,是不言自明的):
      var gulp = require('gulp'), 
        // Nodemon is Node.js module to reload the application when
          any file change. 
       nodemon = require('gulp-nodemon'), 
       plumber = require('gulp-plumber'), 
       // Live reload is browser plugin to synchronize the
         application after the server side changes 
         livereload = require('gulp-livereload'); 
           gulp.task('develop', function () { 
             livereload.listen(); 
             nodemon({ 
               script: 'app.js', 
               ext: 'js ejs', 
               stdout: false 
             }).on('readable', function () { 
             this.stdout.on('data', function (chunk) { 
               if (/^Express server listening on port/.test(chunk))
               { 
                 livereload.changed(__dirname); 
               } 
             }); 
             this.stdout.pipe(process.stdout); 
             this.stderr.pipe(process.stderr); 
           }); 
         }); 
         // We can name it all gulp tasks, we have an alias as develop
          to call default task, on high scale applications we can have
          many tasks with or without alias. 
         gulp.task('default', [ 
           'develop' 
         ]);

最后一个文件是 README.md 文件。通常,这个文件被 GitHub、Bitbucket 和 NPM 用来存储关于项目的重要信息,比如安装过程、依赖关系和代码示例等。

  1. 创建一个名为 README.md 的文件,并添加以下代码:
      # Node.js chat application with Socket.io 

非常简单但非常有用的文件。这个 Markdown 文件将被呈现为一个带有这个字符串作为标题的 HTML 文件。

提示

你可以在这个链接中阅读更多关于 Markdown 文件的信息:daringfireball.net/projects/markdown/

创建 app.js 文件

正如我们在前几章中已经看到的,所有 Node.js 应用程序的基础是我们设置所有依赖项并实例化应用程序的文件。在这种情况下,我们使用 app.js 文件,但你可以使用任何名称。

创建一个名为 app.js 的文件,并添加以下代码:

      // Node dependencies 
      var express = require('express'); 
      var path = require('path'); 
      var favicon = require('serve-favicon'); 
      var logger = require('morgan'); 
      var cookieParser = require('cookie-parser'); 
      var bodyParser = require('body-parser'); 
      // Setup application routes 
      var routes = require('./routes/index'); 
      // Create a Express application 
      var app = express(); 
      // Defining the env variable process for development 
      var env = process.env.NODE_ENV || 'development'; 
        app.locals.ENV = env; 
        app.locals.ENV_DEVELOPMENT = env == 'development'; 
      // Setup view engine to use EJS (Embedded JavaScript) 
        app.set('views', path.join(__dirname, 'views')); 
        app.set('view engine', 'ejs'); 
      // uncommented this line to use a favicon in your application 
      // app.use(favicon(__dirname + '/public/img/favicon.ico')); 
        app.use(logger('dev')); 
        app.use(bodyParser.json()); 
        app.use(bodyParser.urlencoded({ 
          extended: true 
        })); 
        app.use(cookieParser()); 
        app.use(express.static(path.join(__dirname, 'public'))); 
      // Setup all routes to listen on routes file (this came from
         routes variable) 
        app.use('/', routes); 
      // Setup a  404 error handler 
        app.use(function(req, res, next) { 
          var err = new Error('Not Found'); 
          err.status = 404; 
          next(err); 
        }); 
      // Print the error stacktrace 
        if (app.get('env') === 'development') { 
          app.use(function(err, req, res, next) { 
            res.status(err.status || 500); 
            res.render('error', { 
              message: err.message, 
              error: err, 
              title: 'error' 
            }); 
          }); 
        } 
      // No stacktraces on production 
        app.use(function(err, req, res, next) { 
          res.status(err.status || 500); 
          res.render('error', { 
            message: err.message, 
            error: {}, 
            title: 'error' 
          }); 
        }); 
        module.exports = app; 
        // Exports all the application configuration 
          app.set('port', process.env.PORT || 3000); 
       // Setup the server port and give a user message 
       var server = app.listen(app.get('port'), function() { 
         console.log('Express server listening on port ' +
           server.address().port); 
       }); 

上面的代码已经被完全注释了。你需要理解前面的代码发生了什么的所有信息都在注释行中;在这里,我们几乎和前面的例子中有相同的配置。

创建路由文件

现在让我们创建路由文件。

在 routes 文件夹中,创建一个名为 index.js 的新文件,并添加以下代码:

    // Import Express and Router 
      var express = require('express'); 
      var router = express.Router(); 
    // Get 
      router.get('/', function(req, res) { 
        res.render('index', { 
          title: 'Socket.io chat application', 
          lead: 'Insert your user name and start talk' 
        }); 
      }); 
      module.exports = router;

因为我们正在构建一个简单的聊天应用程序,所以我们只有一个路由。

创建应用程序视图

我们的下一步是构建应用程序视图文件,所以我们将在视图目录中使用.ejs 扩展名创建新文件。

  1. 创建一个名为 error.ejs 的新文件,并添加以下代码:
      <% include header %> 
         <div class="container"> 
           <h1><%- error.status %></h1> 
           <h4><%- message %></h4> 
          <p><%- error.stack %></p> 
        </div> 
      <% include footer %> 

  1. 创建一个名为 footer.ejs 的文件,并添加以下代码行:
      <script src="img/socket.io-1.4.5.js"></script> 
      <script src="img/main.js"></script> 
      </body> 
      </html> 

请注意,我们已经从内容交付网络CDN)中包含了 Socket.io 客户端文件。不要担心 footer.ejs 末尾的 main.js 文件;我们将在下面创建这个文件。

  1. 创建一个名为 header.ejs 的文件,并添加以下代码行:
      <!doctype html> 
      <html lang="en"> 
      <head> 
        <meta charset="UTF-8"> 
        <title><%- title %></title> 
        <meta name="viewport" content="width=device-width,
          initial-scale=1"> 
         <link rel="stylesheet" href="/css/style.css"> 
       </head> 
       <body> 
         <div class="site-wrapper"> 
           <div class="site-wrapper-inner"> 
             <div class="cover-container"> 
               <div class="masthead clearfix"> 
                 <div class="inner"> 
                   <h3 class="masthead-brand">
                     Node.js 6 Blueprints Book</h3>
                   <nav class="nav nav-masthead"> 
                     <a  class="active"  href="/">Home</a> 
                   </nav> 
                 </div> 
               </div>

  1. 创建一个名为 index.ejs 的文件,并添加以下代码行:
      <% include header %> 
        <div class="inner cover" id="app"> 
          <h1 class="cover-heading"><%- title %>></h1> 
          <p class="lead"><%- lead %></p> 
          <div class="chat-wrapper"> 
            <div id="user-form" class="row"> 
              <div class="col-md-12"> 
                <form> 
                  <div class="input-group input-group-lg"> 
                    <input id="username" class="form-control"
                      type="text" placeholder="Your name or
                      nickname..."> 
                    <span class="input-group-btn"> 
                      <input type="submit" class="btn btn-success
                      btn-lg" value="Enter"> 
                    </span> 
                  </div> 
                </form> 
              </div> 
            </div> 
            <div id="message-area" class="row" style="display:none"> 
              <div class="col-xs-9"> 
                <div class="card card-inverse"> 
                  <div class="card-header card-success"> 
                      Messages 
                  </div> 
                  <div class="card-block" id="chat-block"> 
                    <ul id="chat" class="list-unstyled"> 
                    </ul> 
                  </div> 
                  <div class="card-footer"> 
                    <form id="message-form" autocomplete="off"> 
                      <div class="input-group input-group-sm"> 
                        <input id="message" class="form-control
                         input-sm" type="text" placeholder="Type here...|">
                        <span class="input-group-btn"> 
                        <input type="submit" class="btn btn-success
                         btn-sm" value="Send message"> 
                      </span> 
                      </div> 
                    </form> 
                  </div> 
                </div> 
              </div> 
              <div class="col-xs-3"> 
                <div class="card card-inverse"> 
                  <div class="card-header card-success"
                    id="online-users-header"> 
                    <span class="card-title">Users in the rooom:</span> 
                  </div> 
                  <div class="card-block" id="online-users-block"> 
                    <ul id="users"></ul> 
                  </div> 
                </div> 
              </div> 
            </div> 
          </div>
        </div> 
        <div class="mastfoot"> 
          <div class="inner"> 
            <p>&copy; 2016 <a href="$">Node.js 6 Blueprints Book</a>,
             by <a href="https://twitter.com/newaeonweb">@newaeonweb
             </a>
            </p> 
          </div> 
        </div> 
      </div>
      </div> 
      </div> 
      <% include footer %> 

请注意,我们正在使用关于Bootstrap 4的一个示例中的 HTML 标记。你可以在这里看到更多示例:v4-alpha.getbootstrap.com/examples/

使用 Bower 安装前端组件

正如我们在前面的例子中所看到的,我们使用 CDN 来提供 CSS 文件和一些 JavaScript 文件用于示例应用程序。在这一步中,我们将介绍一个广泛使用的依赖管理工具,称为Bower,用于处理诸如 Twitter Bootstrap 之类的前端框架:

  1. 打开你的终端/Shell 并输入以下命令:
npm install bower -g

前面的命令在你的机器上全局安装了 Bower。

  1. 在根项目文件夹中,输入以下命令:
bower install bootstrap#v4.0.0-alpha

前面的命令将在 public/components 文件夹中安装 Bootstrap,正如我们在下面的图片中所看到的:

使用 Bower 安装前端组件

组件文件夹的屏幕截图

请注意,前面的命令也会添加 jQuery,因为 Bootstrap 依赖于 jQuery 库。让我们在 header.ejs 和 footer.ejs 中添加链接:

  1. 打开 views/header.ejs 并添加以下代码:
      <link rel="stylesheet" href="components/bootstrap/dist/css
        /bootstrap.min.css">

  1. 打开 footer.ejs 并添加以下代码:
      <script src="img/jquery.min.js"></script> 
      <script src="img/bootstrap.min.js">
      </script> 

添加一些 CSS

现在让我们插入一些 CSS 代码来美化我们的示例页面:

  1. 在 public/css 中创建一个名为 style.css 的新文件。

  2. 将以下代码添加到 style.css 中:

      a, 
      a:focus, 
      a:hover { 
        color: #fff; 
      } 
      html, 
      body { 
        height: 100%; 
        background-color: #068555; 
      } 
      body { 
        color: #fff; 
      } 
      /* Extra markup and styles for table-esque vertical and
       horizontal centering */ 
      .site-wrapper { 
        display: table; 
        width: 100%; 
        height: 100%; /* For at least Firefox */ 
        min-height: 100%; 
        -webkit-box-shadow: inset 0 0 5rem rgba(0,0,0,.5); 
          box-shadow: inset 0 0 5rem rgba(0,0,0,.5);
      } 
      .site-wrapper-inner { 
        display: table-cell; 
        vertical-align: top; 
      } 
      .cover-container { 
        margin-right: auto; 
        margin-left: auto; 
      } 
      .inner { 
        padding: 2rem; 
      } 
      .card { 
        color: #414141; 
      } 
      .card-block { 
        background-color: #fff; 
      } 
      .masthead { 
        margin-bottom: 2rem; 
      } 
      .masthead-brand { 
        margin-bottom: 0; 
      } 
      .nav-masthead a { 
        padding: .25rem 0; 
        font-weight: bold; 
        color: rgba(255,255,255,.5); 
        background-color: transparent; 
        border-bottom: .25rem solid transparent; 
      } 
      .nav-masthead a:hover, 
      .nav-masthead a:focus { 
        text-decoration: none; 
        border-bottom-color: rgba(255,255,255,.25); 
      } 
      .nav-masthead a + a { 
        margin-left: 1rem; 
      } 
      .nav-masthead .active { 
        color: #fff; 
        border-bottom-color: #fff; 
      } 
      #users { 
        display: block; 
      } 
      @media (min-width: 48em) { 
        .masthead-brand { 
          float: left; 
        } 
        .nav-masthead { 
          float: right; 
        } 
      } 
      .cover { 
        padding: 0 1.5rem; 
      } 
      .cover-heading, .lead { 
        text-align: center; 
      } 
      .cover .btn-lg { 
        padding: .75rem 1.25rem; 
        font-weight: bold; 
      } 
      .mastfoot { 
        color: rgba(255,255,255,.5); 
      } 
      @media (min-width: 40em) { 
        /* Pull out the header and footer */ 
        .masthead { 
          position: fixed; 
          top: 0; 
        } 
        .mastfoot { 
          position: fixed; 
          bottom: 0; 
        } 
        /* Start the vertical centering */ 
        .site-wrapper-inner { 
          vertical-align: middle; 
        } 
        /* Handle the widths */ 
        .masthead, 
        .mastfoot, 
        .cover-container { 
          width: 100%;
        /* Must be percentage or pixels for horizontal alignment */ 
        } 
      } 
      @media (min-width: 62em) { 
        .masthead, 
        .mastfoot, 
        .cover-container { 
          width: 62rem; 
        } 
      }

我们对样式表进行了一些修改,以获得我们想要的书籍示例结果。

在这个阶段,我们有了主屏幕。

  1. 打开你的终端/Shell 并输入以下命令:
gulp 

  1. 转到 http://localhost:3000/,你将看到以下结果:添加一些 CSS

主屏幕的屏幕截图

添加实时重新加载插件

如前所述,我们将使用 livereload 插件。这个插件负责在我们更改应用程序文件时更新浏览器。现在让我们看看如何在我们的示例中实现它:

  1. 请记住,我们在本章的开头创建了 gulpfile.js 文件,所以我们已经根据以下行配置了 livereload 任务:
      gulp.task('develop', function () { 
        livereload.listen(); 
        nodemon({ 
          script: 'app.js', 
          // map every file with .js, .ejs, extension and relaunch
            the application 
          ext: 'js ejs', 
          stdout: false 
        })
        .on('readable', function () { 
          this.stdout.on('data', function (chunk) { 
            if (/^Express server listening on port/.test(chunk)) { 
              livereload.changed(__dirname); 
            } 
          }); 
          this.stdout.pipe(process.stdout); 
          this.stderr.pipe(process.stderr); 
        }); 
      });

提示

你可以在这里阅读更多关于 gulp-livereload 插件的信息:github.com/vohof/gulp-livereload

  1. 打开 views/header.ejs 并在样式表链接之后添加以下代码:
      <% if (ENV_DEVELOPMENT) { %> 
        <script src="img/livereload.js"></script> 
      <% } %>

这些代码告诉应用程序在使用开发环境时注入 livereload 插件。

  1. 现在每次更改文件时,我们可以在终端上看到以下消息:添加实时重新加载插件

终端屏幕截图,带有 livereload 消息

  1. 但请记住,我们配置了 livereload 任务仅映射.js 和.ejs 文件,正如我们在以下代码的突出显示行中所看到的:
      livereload.listen(); 
      nodemon({ 
        script: 'app.js', 
        ext: 'js ejs', 
        stdout: false 
      })

要映射其他文件格式,必须将文件扩展名添加到该行。

检查应用程序文件夹结构

一切就绪后,我们现在需要检查应用程序的目录结构。这次,您的应用程序应该看起来像以下的屏幕截图:

检查应用程序文件夹结构

应用程序结构的屏幕截图

提示

请记住,您可以在 Packt Publishing 网站(www.packtpub.com)以及 GitHub 上的官方书籍存储库上下载本书的示例代码。

创建 Socket.io 事件

现在是时候在我们的服务器上创建 socket.io 事件问题了。socket.io 允许您发送和接收任何类型的数据事件:

打开根文件夹中的 app.js,并在文件末尾添加以下代码:

    // Starting with socket.io 
    var io = require('socket.io').listen(server); 
    // Create an Array to hold users 
    var userList = []; 
    // Create an Array to hold connections 
    var connections = []; 
    // Start connection listener 
    io.sockets.on('connection', function (socket) { 
      connections.push(socket); 
      console.log("Connected:", connections.length ); 
      // Setup Disconnect user 
      socket.on('disconnect', function (data) { 
        if (socket.username) { 
          userList.splice(userList.indexOf(socket.username), 1); 
          updateUsernames(); 
        } 
        connections.splice(connections.indexOf(socket), 1); 
        console.log("Disconnected:" , connections.length ); 
      }); 
      // Setup new messages 
      socket.on('send message', function (data) { 
        io.sockets.emit('new message', { msg: data, user:
         socket.username }); 
      }); 
      // New User 
      socket.on('new user', function (data, callback) { 
        callback(!!data); 
        socket.username = data; 
        userList.push(socket.username); 
        updateUsernames(); 
      }); 
      function updateUsernames() { 
        io.sockets.emit('get userList', userList); 
      } 
    });

在客户端添加 socket.io 行为

如前所述,socket.io 在先前的代码中使用事件,在那里我们配置了我们的服务器来发送和接收事件。现在我们设置我们的客户端来接收和发送事件。在这一步中,我们将使用 jQuery 库来协助完成这项任务,因为我们已经在项目中使用了该库:

  1. 在 public/js 文件夹中,创建一个名为 main.js 的新文件。

  2. 将以下代码放入 main.js 文件中:

      (function() { 
         // Grab all HTML elements into variables 
         var socket = io.connect(); 
         var $messageForm = $('#message-form'); 
         var $message = $('#message'); 
         var $chat = $('#chat'); 
         var $messageArea = $('#message-area'); 
         var $userForm = $('#user-form'); 
         var $users = $('#users'); 
         var $onlineUsersHeader = $('#online-users-header'); 
         var $username = $('#username'); 
         // Form submit to send messages 
         $messageForm.submit(function(e) { 
           e.preventDefault(); 
           socket.emit('send message', $message.val()); 
           $message.val(''); 
         }); 
        // When a new message is sent, print username and time to
          interface 
       socket.on('new message', function(data) { 
         var currentHours = new Date().getHours() > 9 ? new
          Date().getHours() : ('0' + new Date().getHours()) 
         var currentMinutes = new Date().getMinutes() > 9 ? new
          Date().getMinutes() : ('0' + new Date().getMinutes()) 
           data.msg ? ( 
             $chat.append(`<li>[${currentHours}:${currentMinutes}]
               <strong> ${data.user}: </strong>${data.msg}</li>`) )
             : alert('Blank message not allow!'); 
           }); 
          // Form submit to username 
          $userForm.submit(function(e) { 
            e.preventDefault(); 
            socket.emit('new user', $username.val(), function(data) { 
              data ? ( 
                $userForm.hide(), 
                $messageArea.show() 
              ) : alert('Ohps. What's your name!') 
            }); 
            $username.val(''); 
          }); 
          // get all users connected on localhost:3000 and print a list 
          socket.on('get userList', function(data) { 
            var html = ''; 
            for (i = 0; i < data.length; i++) { 
              html += `<li class="list-item"><strong>${data[i]}
              </strong></li>`; 
            } 
            $onlineUsersHeader.html(`<span class="card-title">
              Users in the room: </span><span class="label label-
              success">${data.length}</span>`); 
            $users.html(html); 
          }); 
        })(); 

让我们运行应用程序,看看终端上会发生什么。

  1. 在根项目上打开终端/ shell,并输入以下命令:
gulp

您的终端输出将如下所示:

在客户端添加 socket.io 行为

应用程序运行时的输出终端屏幕截图

在这里,我们可以看到我们只有一个连接。但是,如果我们在新的浏览器窗口中打开 http://localhost:3000/,甚至在另一个浏览器中打开,我们可以看到两个连接,依此类推。

启动聊天应用程序

现在我们可以同时在两个窗口中使用我们的应用程序:

  1. 打开您的终端/ shell,并输入以下命令:
gulp

  1. 转到 http://localhost:3000/,输入名称John Doe,您将看到以下结果:启动聊天应用程序

John Doe 用户的屏幕截图

我们可以看到只有一个用户,现在让我们用相同的 socket 打开另一个连接。使用一个新窗口或另一个浏览器。

  1. 转到 http://localhost:3000/,并输入名称Max Smith。您应该在右侧面板上看到以下结果:启动聊天应用程序

用户面板的屏幕截图

现在我们有两个用户。让我们开始交谈...

  1. John Doe屏幕上,输入此消息:有人在吗?

检查Max Smith屏幕,您将看到John的消息出现,就像下面的图片中所示的那样:

启动聊天应用程序

Max Smith 屏幕聊天的屏幕截图

  1. 返回到 John Doe 屏幕并检查消息并回答它,就像我们在下面的图片中所做的那样:启动聊天应用程序

John Doe 屏幕聊天的屏幕截图

总结

在本章中,我们讨论了一些关于 Node.js 实时应用程序的非常重要的概念,使用了 Node.js 和 Socket.io。

我们已经看到如何使用几行 jQuery 在用户之间实时交换消息。此外,我们还讨论了一些在现代 Web 应用程序开发中非常重要的主题,如使用 Bower 进行前端依赖项和使用 livereload 插件的 Gulp 任务管理器。

请记住,所有这些工具都可以通过 Node Package Manager(NPM)在 Node.js 生态系统中获得。

在下一章中,我们将看到如何使用完全基于 Node.js 构建的内容管理系统(CMS)来构建博客应用程序。

第八章:使用 Keystone CMS 创建博客

在本章中,我们讨论了完全使用 Node.js 制作的 CMS,称为Keystone的用法。

KeystoneJS自述为一个创建数据库驱动网站的开源平台。它已经有一个构建 Web 应用程序和强大博客的核心引擎,但它远不止于此。使用 Keystone.js 框架可以构建任何东西。

Keystone CMS 的主要吸引力之一是它使用 Express 框架和 Mongoose ODM,这两个工具我们在本书中已经使用过。

由于它是一个非常新的框架,它只有一个简单的默认主题,使用了 Bootstrap 框架,但是 Keystone 团队计划包括自定义新主题的选项,这将在不久的将来实现。

Keystone 使用了模型视图模板模式,非常类似于模型视图演示等模式。

在这一章中,我们将看到如何使用框架的所有功能构建一个新主题,以及如何通过新功能扩展它。

在这一章中,我们将涵盖以下主题:

  • 安装 KeystoneJS

  • KeystoneJS 的结构和特性

  • 如何使用简单样式定制

  • 处理主题以及如何创建新主题

  • 扩展核心功能以创建模型和视图。

我们正在构建什么

对于本章,我们将以一个简单的博客作为基础。我们将看到如何扩展它并创建可以通过控制面板管理的新页面,并且我们将得到一个与以下图像非常相似的结果:

我们正在构建什么

Keystone 博客主题首页

安装 Keystone 框架

与之前的章节一样,我们将使用官方的 Keystone.js yeoman 生成器。

提示

您可以在此链接找到有关 KeystoneJS 的更多信息:keystonejs.com/

让我们安装生成器。打开您的终端/Shell 并输入以下命令:

npm install keystone -g

创建脚手架应用程序

现在是时候创建一个新文件夹并开始开发我们的博客应用程序了:

  1. 创建一个名为 chapter-08 的文件夹。

  2. 在 chapter-08 文件夹中打开您的终端/Shell,并输入以下命令:

yo keystone

在此命令之后,keystone.js 将触发一系列关于应用程序基本配置的问题;您必须回答这些问题,如下截图所示:

创建脚手架应用程序

Keystone 生成器的提示问题

  1. 在所有生成器任务结束后,我们可以在终端窗口上看到以下输出:
Your KeystoneJS project is ready to go!
 For help getting started, visit http://keystonejs.com/guide
 We've included a test Mandrill API Key, which will simulate
        email 
sending but not actually send emails. Please replace
        it with your own 
when you are ready.
 We've included a demo Cloudinary Account, which is reset daily. 
 Please configure your own account or use the Local Image field
        instead 
before sending your site live.
 To start your new website, run "npm start".

请注意,在启动应用程序之前,我们需要纠正两个小错误。在撰写本文时,生成器存在此故障;但是,当书籍发布时,这个问题应该已经被修复。如果没有,这是解决此问题的方法。

修复 lint 错误和 admin 对象名称

  1. 在项目根目录中打开 gulpfile.js 并删除有关 lint 任务的行:
      watch:lint

  1. 修复管理用户名,打开根文件夹中的 Keystone.js 文件并替换以下代码:
      keystone.set('nav', { 
        posts: ['posts', 'post-categories'], 
        galleries: 'galleries', 
        enquiries: 'enquiries', 
        userAdmins: 'user-admins' 
      }); 

就这些了,我们已经有了我们的博客。让我们来检查一下结果。

运行 Keystone 博客

  1. 打开终端/Shell 并输入以下命令:
gulp 

  1. 转到 http://localhost:3000/;您应该看到以下结果:运行 Keystone 博客

Keystone 主页

如前所述,界面非常简单。它可以查看生成器生成的默认信息,包括有关用户和密码的信息。

  1. 点击右上角的登录链接,并使用上一个截图中的用户名和密码填写登录表单。结果将是控制面板,如下图所示:运行 Keystone 博客

Keystone 控制面板

每个链接都有一个表单,用于插入博客的数据,但现在不用担心这个;在本章后面,我们将看到如何使用管理面板。

正如我们在之前的图片中所看到的,布局非常简单。然而,这个框架的亮点不是它的视觉外观,而是它的核心引擎构建强大应用程序的能力。

提示

您可以在官方网站keystonejs.com/上了解更多关于 Keystone 的信息。

Keystone 引擎的解剖

在我们直接进入代码之前,我们将了解 Keystone 的目录结构是如何工作的。

启动应用程序后,我们将得到以下结果:

Keystone 引擎的解剖

Keystone 目录结构

这里是每个目录/文件夹的描述:

文件夹名称 文件夹路径 描述
模型 /models/ 应用程序数据库模型。
公共 /public/ 图像、JavaScript、样式表和字体。
路由 /routes//routes/views 视图控制器(在 Restful API 上,我们可以使用一个名为 API 的文件夹)。
模板 /templates//templates/emails//templates/layouts//templates/mixins//templates/views 应用程序视图模板。
更新 /updates/ 迁移脚本和数据库填充。

此外,我们在根文件夹中有以下文件:

  • .editorconfig:设置编辑器的缩进

  • .env:设置 Cloudnary Cloud 凭据

  • .gitignore:Git 源控制的忽略文件

  • gulpfile.js:应用程序任务

  • keystone.js:引导应用程序

  • package.json:项目配置和 NPM 模块

  • procfile:Heroku部署的配置

在接下来的行中,我们将深入了解每个部分的功能。

提示

路由文件夹中有一些文件,我们现在不会解释,但不用担心;我们将在下一个主题中看到这些文件。

更改默认的 bootstrap 主题

我们将展示两种自定义博客的方法:一种是表面的,只改变样式表,另一种是更深入的,改变整个页面的标记。

对于样式表的更改,我们正在使用bootswatch.com/免费的 Bootstrap 主题。

bootstrap 框架非常灵活;我们将使用一个名为 superhero 的主题。

  1. 转到bootswatch.com/superhero/_variables.scss URL。

  2. 复制页面内容。

  3. 在 public/styles/boostrap/bootstrap 中,创建一个名为 _theme_variables.scss 的新文件,并粘贴从 Bootswatch 页面复制的代码。

  4. 打开 public/styles/bootstrap/_bootstrap.scss 并替换以下行:

      // Core variables and mixins 
      @import "bootstrap/_theme_variables"; 
      @import "bootstrap/mixins";

现在我们将重复步骤 12,但现在使用不同的 URL。

  1. 转到bootswatch.com/superhero/_bootswatch.scss URL。

  2. 复制页面内容。

  3. 在 public/styles/bootstrap 中创建一个名为 _bootswatch.scss 的文件,并粘贴内容。

  4. 打开 public/styles/bootstrap/_bootstrap.scss 并替换以下突出显示的行:

      // Bootswatch overhide classes
      @import "bootswatch";

  1. 完成。现在我们有了一个与 keystone.js 采用的标准布局不同的布局,让我们看看结果。打开您的终端/Shell 并输入以下命令:
 gulp 

  1. 转到 URL:http://localhost:3000/,您应该会看到以下结果:更改默认的 bootstrap 主题

Keystone 主屏幕

通过这个小改变,我们已经可以看到所取得的结果。然而,这是一个非常表面的定制,因为我们没有改变任何 HTML 标记文件。

在之前的图片中,我们可以看到我们只是改变了页面的颜色,因为它保持了标记不变,只使用了一个 bootstrap 主题。

在下一个示例中,我们将看到如何修改应用程序的整个结构。

修改 KeystoneJS 核心模板路径

现在让我们对模板目录进行一些重构。

  1. 在模板中,创建一个名为 default 的文件夹。

  2. 将模板文件夹中的所有文件移动到新的 default 文件夹中。

  3. 复制默认文件夹中的所有内容,并将它们粘贴到一个名为 newBlog 的新文件夹中。

结果将是以下截图,但我们需要更改 keystone.js 文件以配置新文件夹:

修改 KeystoneJS 核心模板路径

模板文件夹结构

  1. 从根文件夹打开 keystone.js 文件并更新以下行:
      'views': 'templates/themes/newBlog/views', 
      'emails': 'templates/themes/newBlog/emails', 

完成。我们已经创建了一个文件夹来保存所有我们的主题。

构建我们自己的主题

现在我们将更改主题标记。这意味着我们将编辑 newBlog 主题内的所有 HTML 文件。我们使用github.com/BlackrockDigital/startbootstrap-clean-blog提供的免费模板作为参考和来源。我们的目标是拥有类似以下截图的布局:

构建我们自己的主题

Keystone 主屏幕

  1. 打开模板/主题/newBlog/layouts/default.swig 并将以下代码添加到标记中:
      {# Custom Fonts #} 
      <link href="http://maxcdn.bootstrapcdn.com/font-awesome/4.1.0
       /css/font-awesome.min.css" rel="stylesheet" type="text/css"> 
      <link href='http://fonts.googleapis.com
       /css?family=Lora:400,700,400italic,700italic'
       rel='stylesheet' type='text/css'> 
      <link href='http://fonts.googleapis.com      
       /css?family=Open+Sans:300italic,400italic,600italic,
       700italic,800italic,400,300,600,700,800' rel='stylesheet'
       type='text/css'> 

  1. 删除{# HEADER #}和{# JAVASCRIPT #}注释之间的所有行。

提示

请注意,此操作将删除 default.swig 文件底部的 body 标记后的所有内容和 JavaScript 链接。

  1. 现在将以下代码行放在{# HEADER #}和{# JAVASCRIPT #}注释之间:
      <div id="header"> 
      {# Customise your sites navigation by changing the 
       navLinks Array in ./routes/middleware.js 
        ... or completely change this header to suit your design. #} 

      <!-- Navigation --> 
      <nav class="navbar navbar-default navbar-custom
       navbar-fixed-top"> 
        <div class="container-fluid"> 
          <!-- Brand and toggle get grouped for better mobile
           display --> 
          <div class="navbar-header page-scroll"> 
            <button type="button" class="navbar-toggle"
             data-toggle="collapse" data-target="#bs-example-navbar-
             collapse-1"> 
              <span class="sr-only">Toggle navigation</span> 
              <span class="icon-bar"></span> 
              <span class="icon-bar"></span> 
              <span class="icon-bar"></span> 
            </button> 
            <a class="navbar-brand" href="/">newBlog</a> 
          </div> 
          <!-- Collect the nav links, forms, and other content
            for toggling --> 
          <div class="collapse navbar-collapse" id="bs-example
           -navbar-collapse-1"> 
            <ul class="nav navbar-nav navbar-left"> 
              {%- for link in navLinks -%} 
                {%- set linkClass = '' -%} 
                {%- if link.key == section -%} 
                  {%- set linkClass = ' class="active"' -%} 
              {%- endif %} 
              <li{{ linkClass | safe }}> 
                <a href="{{ link.href }}">{{ link.label }}</a> 
              </li> 
              {%- endfor %} 
              </ul> 
                <ul class="nav navbar-nav navbar-right"> 
                  {% if user -%} 
                    {%- if user.canAccessKeystone -%} 
                      <li><a href="/keystone">Open Keystone</a>
                      </li> 
                    {%- endif -%} 
                      <li><a href="/keystone/signout">Sign Out</a>
                      </li> 
                    {%- else -%} 
                      <li><a href="/keystone/signin">Sign In</a>
                      </li> 
                    {%- endif %} 
                </ul> 
          </div> 
          <!-- /.navbar-collapse --> 
          </div> 
          <!-- /.container --> 
        </nav> 
        <!-- Page Header --> 
        <header class="intro-header"> 
        <div class="container"> 
          <div class="row"> 
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-
             md-offset-1"> 
            <div class="site-heading"> 
              <h1>Node.js 6 Blueprints</h1> 
              <hr class="small"> 
              <span class="subheading">A Clean Blog using 
               KeystoneJS</span> 
            </div> 
          </div> 
        </div> 
      </div> 
      </header> 
      </div> 

      {# BODY #} 
      <div id="body"> 
      {# NOTE: There is no .container wrapping class around body
        blocks to allow more flexibility in design. 
      Remember to include it in your templates when you override
        the intro and content blocks! #} 

      {# The Intro block appears above flash messages (used for
       temporary information display) #} 
      {%- block intro -%}{%- endblock -%} 

      {# Flash messages allow you to display once-off status messages
       to users, e.g. form 
      validation errors, success messages, etc. #} 
      {{ FlashMessages.renderMessages(messages) }} 

      {# The content block should contain the body of your templates
       content #} 
      {%- block content -%}{%- endblock -%} 
      </div> 

  1. 打开模板/主题/newBlog/views/blog.swig 并用以下代码替换代码:
      {% extends "../layouts/default.swig" %} 

      {% macro showPost(post) %} 
      <div class="post" data-ks-editable="editable(user, { list:
        'Post', id: post.id })"> 
        <div class="post-preview"> 
          {% if post.image.exists %} 
            <img src="img/{{ post._.image.fit(400,300) }}" class="img
              text-center" width="100%" height="260px"> 
          {% endif %} 
          <a href="/blog/post/{{ post.slug }}"> 
            <h2 class="post-title"> 
              {{ post.title }} 
            </h2> 
            <h3 class="post-subtitle"> 
              {{ post.content.brief | safe }} 
            </h3> 
          </a> 
          <p class="post-meta">Posted by <a href="#"> 
            {% if post.author %} {{ post.author.name.first }}
            {% endif %} 
          </a>
            {% if post.publishedDate %} 
          on
            {{ post._.publishedDate.format("MMMM Do, YYYY") }} 
            {% endif %} 
            {% if post.categories and post.categories.length %} 
          in 
          {% for cat in post.categories %} 
          <a href="/blog/{{ cat.key }}">{{ cat.name }}</a> 
            {% if loop.index < post.categories.length - 1 %},
            {% endif %} 
          {% endfor %} 
          {% endif %} 
        </p> 
        {% if post.content.extended %} 
        <a class="read-more" href="/blog/post/{{ post.slug }}">
          Read more...</a> 
        {% endif %} 
      </div> 
      <hr> 
      </div> 
      {% endmacro %} 

      {% block intro %} 
        <div class="container"> 
        {% set title = "Blog" %} 
          {% if data.category %} 
            {% set title = data.category.name %} 
          {% endif %} 
          <h1>{{ title }}</h1> 
        </div> 
      {% endblock %} 

      {% block content %} 
      <div class="container"> 
        <div class="row"> 
          <div class="col-sm-8 col-md-9"> 
            {% if filters.category and not data.category %} 
              <h3 class="text-muted">Invalid Category.</h3> 
            {% else %} 
            {% if data.posts.results.length %} 
              {% if data.posts.totalPages > 1 %} 
                <h4 class="text-weight-normal">Showing 
                  <strong>{{ data.posts.first }}</strong> 
                  to 
                  <strong>{{ data.posts.last }}</strong> 
                  of 
                  <strong>{{ data.posts.total }}</strong> 
                  posts. 
                </h4> 
             {% else %} 
            <h4 class="text-weight-normal">Showing 
              {{ utils.plural(data.posts.results.length, "*
               post") }}
            </h4> 
          {% endif %} 
          <div class="blog"> 
            {% for post in data.posts.results %} 
              {{ showPost(post) }}
            {% endfor %} 
          </div> 
          {% if data.posts.totalPages > 1 %} 
          <ul class="pagination"> 
            {% if data.posts.previous %} 
            <li> 
              <a href="?page={{ data.posts.previous }}"> 
                <span class="glyphicon glyphicon-chevron-left">
                </span> 
              </a> 
            </li> 
            {% else %} 
            <li class="disabled"> 
              <a href="?page=1"> 
                <span class="glyphicon glyphicon-chevron-left">
                </span> 
              </a> 
            </li> 
            {% endif %} 
            {% for p in data.posts.pages %} 
              <li class="{% if data.posts.currentPage == p %}
                active{% endif %}"> 
              <a href="?page={% if p == "..." %}{% if i %}
                {{data.posts.totalPages }}{% else %}1{% endif %}
                {% else %}{{ p }}{% endif %}">{{ p }}
              </a> 
              </li> 
            {% endfor %} 
            {% if data.posts.next %} 
            <li> 
              <a href="?page={{ data.posts.next }}"> 
                <span class="glyphicon glyphicon-chevron-right">
                </span> 
              </a> 
            </li> 
            {% else %} 
            <li class="disabled"> 
              <a href="?page={{ data.posts.totalPages }}"> 
                <span class="glyphicon glyphicon-chevron-right">
                </span> 
              </a> 
            </li> 
            {% endif %} 
          </ul> 
          {% endif %} 
          {% else %} 
            {% if data.category %} 
              <h3 class="text-muted">There are no posts in the
                category {{ data.category.name }}.
              </h3> 
            {% else %} 
              <h3 class="text-muted">There are no posts yet.</h3> 
            {% endif %} 
          {% endif %} 
          {% endif %} 
        </div> 
        {% if data.categories.length %} 
          <div class="col-sm-4 col-md-3"> 
            <h2>Categories</h2> 
              <div class="list-group" style="margin-top: 70px;"> 
                <a href="/blog" class="{% if not data.category %}
                  active{% endif %} list-group-item">All Categories
                </a> 
               {% for cat in data.categories %} 
               <a href="/blog/{{ cat.key }}" class="{% if
                 data.category and data.category.id == cat.id %}
                 active{% endif %} list-group-item">{{ cat.name }}
               </a> 
               {% endfor %} 
              </div> 
          </div> 
        {% endif %} 
      </div> 
      </div> 
      {% endblock %} 

  1. 打开模板/主题/newBlog/views/contact.swig 并用以下代码替换代码:
      {% extends "../layouts/default.swig" %} 

      {% block intro %} 
        <div class="container"> 
          <h1>Contact Us</h1> 
        </div> 
      {% endblock %} 

      {% block content %} 
        <div class="container"> 
         {% if enquirySubmitted %} 
           <h3>Thanks for getting in touch.</h3> 
        {% else %} 
          <div class="row control-group"> 
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-
              offset-1"> 
              <form method="post"> 
                <input type="hidden" name="action" value="contact"> 
                  {% set className = "" %} 
                  {% if validationErrors.name %} 
                    {% set className = "has-error" %} 
                  {% endif %} 
                <div class="form-group {{ className }} col-xs-12
                  floating-label-form-group controls"> 
                  <label>Name</label> 
                  <input type="text" name="name.full" value="{{
                   formData['name.full'] | default('') }}" class=
                   "form-control" placeholder="Name"> 
                </div> 
                {% set className = "" %} 
                {% if validationErrors.email %} 
                  {% set className = "has-error" %} 
                {% endif %} 
                <div class="form-group {{ className }} col-xs-12
                  floating-label-form-group controls"> 
                  <label>Email</label> 
                  <input type="email" name="email" value="{{ 
                  formData.email | default('') }}" class=
                  "form-control" placeholder="E-mail"> 
                </div> 
                  <div class="form-group col-xs-12 floating-label-
                    form-group controls"> 
                    <label>Phone</label> 
                    <input type="text" name="phone" value="{{ 
                      formData.phone | default('') }}" placeholder=
                      "Phone Number (Optional)" class="form-control"> 
                  </div> 
                  {% set className = "" %} 
                  {% if validationErrors.enquiryType %} 
                    {% set className = "has-error" %} 
                  {% endif %} 
                  <div class="form-group {{ className }} col-xs-12
                   floating-label-form-group controls"> 
                    <span class="title-label text-muted">
                     What are you contacting us about?
                    </span> 
                    <br> 
                    <select name="enquiryType" class="form-control"> 
                      <option value="">(select one)</option> 
                      {% for type in enquiryTypes %} 
                        {% set selected = "" %} 
                        {% if formData.enquiryType === type.value %} 
                          {% set selected = " selected" %} 
                        {% endif %} 
                      <option value="{{ type.value }}"{{ selected }}>
                        {{ type.label }}</option> 
                      {% endfor %} 
                    </select> 
                  </div> 
                  {% set className = "" %} 
                  {% if validationErrors.message %} 
                    {% set className = "has-error" %} 
                  {% endif %} 
                  <div class="form-group {{ className }} col-xs-12
                    floating-label-form-group controls"> 
                     <label>Message</label> 
                     <textarea rows="5" class="form-control"
                       placeholder="Message" name="message">
                     </textarea>
                     {{ formData.message }} 
                   </div> 
                   <br> 
                   <div class="row"> 
                     <div class="form-group col-xs-12"> 
                       <button type="submit" class="btn
                         btn-default">Send</button> 
                     </div> 
                   </div> 
                 </form> 
               </div> 
             </div> 
          {% endif %} 
          </div> 
      {% endblock %} 

  1. 打开模板/主题/newBlog/views/gallery.swig 并用以下代码替换代码:
      {% extends "../layouts/default.swig" %} 

      {% block intro %} 
      <div class="container"> 
        <h1>Gallery</h1> 
      </div> 
      {% endblock %} 

      {% block content %} 
        <div class="container"> 
        {% if galleries.length %} 
          {% for gallery in galleries %} 
            <h2>{{ gallery.name }} 
            {% if gallery.publishedDate %} 
              <span class="pull-right text-muted">{{ 
                gallery._.publishedDate.format("Do MMM YYYY") }}
              </span> 
        {% endif %} 
            </h2> 
            <div class="row"> 
            {% if gallery.heroImage.exists %} 
              <div class="gallery-image"> 
                <img src="img/{{ gallery._.heroImage.limit(0.73,200) }}">
              </div> 
              <br> 
              <hr> 
                <div class="row"> 
                  <div class='list-group gallery'> 
                  {% for image in gallery.images %} 
                  <div class='col-sm-6 col-xs-6 col-md-4 col-lg-4'> 
                    <a class="thumbnail fancybox" rel="ligthbox"
                      href="{{ image.limit(640,480) }}"> 
                    <img class="img-responsive" alt="" src="{{ 
                      image.limit(300,320) }}" /> 
                    </a> 
                  </div> 
                  {% endfor %} 
                </div> 
              </div> 
            {% else %} 
            <div class="row"> 
              <div class='list-group gallery'> 
                {% for image in gallery.images %} 
                <div class='col-sm-6 col-xs-6 col-md-4 col-lg-4'> 
                  <a class="thumbnail fancybox" rel="ligthbox"
                    href="{{ image.limit(640,480) }}"> 
                  <img class="img-responsive" alt="" src="{{ 
                    image.limit(300,320) }}" /> 
                  </a> 
                </div> 
              {% endfor %} 
            </div> 
          </div> 
        {% endif %} 
      </div> 
      {% endfor %} 
      {% else %} 
        <h3 class="text-muted">There are no image galleries yet.</h3> 
      {% endif %} 
      </div> 
      {% endblock %} 

  1. 打开模板/主题/newBlog/views/index.swig 并用以下代码替换代码:
      {% extends "../layouts/default.swig" %} 

      {% block content %} 
        <div class="container"> 
          <div class="row"> 
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-
              offset-1"> 
            {% for post in data.posts %} 
            <div class="post-preview"> 
              <a href="/blog/post/{{ post.slug }}"> 
              <h2 class="post-title"> 
                {{ post.title }} 
              </h2> 
              <h3 class="post-subtitle"> 
                {{ post.content.brief | safe }} 
              </h3> 
              </a> 
              <p class="post-meta">Posted by <span class=
                 "text-primary"> 
                {% if post.author %} {{ post.author.name.first }}
                {% endif %} 
                </span> {% if post.publishedDate %} 
                  on 
                {{ post._.publishedDate.format("MMMM Do, YYYY") }} 
                {% endif %}</p> 
              </div> 
              <hr> 
            {% endfor %} 
            <!-- Pager --> 
            {% if data.posts %} 
            <ul class="pager"> 
              <li class="next"> 
                <a href="/blog">Older Posts &rarr;</a> 
              </li> 
            </ul> 
            {% endif %} 
          </div> 
        </div> 
      </div> 

      {% endblock %} 

请注意,在 index.swig 中,我们添加了一些代码行以在索引页面上显示帖子列表,因此我们需要更改 index.js 控制器。

  1. 打开 routes/views/index.js 并添加以下代码行:
      var keystone = require('keystone'); 

      exports = module.exports = function (req, res) { 

        var view = new keystone.View(req, res); 
        var locals = res.locals; 

          // locals.section is used to set the currently selected 
          // item in the header navigation. 
          locals.section = 'home'; 

          // Add code to show posts on index 
          locals.data = { 
            posts: [] 
          }; 
          view.on('init', function(next) { 
            var q = keystone.list('Post').model.find() 
            .where('state', 'published') 
            .sort('-publishedDate') 
            .populate('author') 
            .limit('4'); 

          q.exec(function(err, results) { 
            locals.data.posts = results; 
            next(err); 
          }); 
        }); 

        // Render the view 
        view.render('index'); 
      };

  1. 打开模板/主题/newBlog/views/post.swig 并用以下代码替换代码:
      {% extends "../layouts/default.swig" %} 

      {% block content %} 
      <article> 
        <div class="container"> 
          <a href="/blog">&larr; back to the blog</a> 
          <div class="row"> 
            <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-
              offset-1"> 
              {% if not data.post %} 
                <h2>Invalid Post.</h2> 
              {% else %} 
                <h1>{{ data.post.title }}</h1> 
                  {% if data.post.publishedDate %} 
                  on 
              {{ data.post._.publishedDate.format("MMMM Do, YYYY") }} 
              {% endif %} 
              {% if data.post.categories and
                data.post.categories.length %} 
              in 
              {% for cat in data.post.categories %} 
                <a href="/blog/{{ cat.key }}">{{ cat.name }}</a> 
              {% if loop.index < data.post.categories.length - 1 %},
              {% endif %}
            {% endfor %} 
          {% endif %} 
          {% if data.post.author %} 
            by {{ data.post.author.name.first }} 
          {% endif %} 
          <div class="post"> 
            {% if data.post.image.exists %} 
              <div class="image-wrap"> 
                <img src="img/{{ data.post._.image.fit(750,450) }}"
                  class="img-responsive"> 
               </div> 
             {% endif %} 
             {{ data.post.content.full | raw }} 
               </div> 
             {% endif %} 
           </div> 
         </div> 
       </div> 
       </article> 
       <hr> 
       {% endblock %} 

通过这一段代码,我们已经完成了 HTML 标记的更改。现在我们需要应用新的样式表。

更改样式表

由于我们选择了 SASS 来处理 keystone.js 设置中的样式表,我们已经拥有了使用SASS功能的一切。

打开 public/styles/site/_variables.scss 并替换以下代码行:

    // Override Bootstrap variables in this file, e.g.
     $font-size-base: 14px;
    // Theme Variables
    $brand-primary: #0085A1;
    $gray-dark: lighten(black, 25%);
    $gray: lighten(black, 50%);
    $white-faded: fade(white, 80%);
    $gray-light: #eee;

请记住,我们使用 http://blackrockdigital.github.io/startbootstrap-clean-blog/index.html 作为参考,我们只挑选了一些代码块。请注意,模板使用的是 LESS 而不是SASS,但在这里我们重新编写所有代码以适应 SASS 语法。

由于空间原因,我们没有在此示例中放置整个样式表。您可以从 Packt Publishing 网站(www.packtpub.com)或直接从 GitHub 书库下载示例代码。

重要的是要注意,我们为示例博客创建了相同的样式表,但我们将LESS语法转换为SASS

  1. 打开 public/styles/site/_layout.scss 并使用代码。

  2. 在 public/styles/site/中创建一个名为 _mixins.scss 的新文件,并添加以下代码行:

      // Mixins 
      @mixin transition-all() { 
        -webkit-transition: all 0.5s; 
        -moz-transition: all 0.5s; 
        transition: all 0.5s; 
      } 
      @mixin background-cover() { 
        -webkit-background-size: cover; 
        -moz-background-size: cover; 
        background-size: cover; 
        -o-background-size: cover; 
      } 
      @mixin serif() { 
        font-family: 'Lora', 'Times New Roman', serif; 
      } 
      @mixin sans-serif () { 
        font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial,
          sans-serif; 
      } 

现在我们只需要编辑 public/styles/site.scss 以包含新的 mixin 文件。

  1. 打开 public/styles/site.scss 并添加以下代码行:
      // Bootstrap 
      // Bootstrap can be removed entirely by deleting this line. 
      @import "bootstrap/bootstrap"; 
      // The easiest way to customise Bootstrap variables while 
      // being able to easily override the source files with new 
      // versions is to override the ones you want in another file. 
      // 
       // You can also add your own custom variables to this file for 
         // use in your site stylesheets. 
      @import "site/variables"; 
      // Add mixins 
      @import "site/mixins"; 
      // Site Styles 
      // =========== 
      // Add your own site style includes here 
      @import "site/layout"; 

  1. 将样本图像 header-bg-1290x1140.jpg 从 sample-images 文件夹添加到 public/images/文件夹中(您可以从 Packt Publishing 网站或 GitHub 官方书页下载所有示例文件)。

添加画廊脚本

正如我们所看到的,默认的 Keystone.js 主题非常简单,只使用了 Bootstrap 框架。现在我们将使用一个名为 Fancybox 的 jQuery 插件来应用新的样式在我们的画廊中。

提示

您可以在官方网站fancybox.net/上获取有关Fancybox的更多信息。

  1. 打开模板/主题/newBlog/layouts/default.swig 并在标记内添加以下突出显示的代码:
      {# Customise the stylesheet for your site by editing
       /public/styles/site.sass #} 
      <link href="/styles/site.css" rel="stylesheet"> 
      <!-- fancyBox --> 
      <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs
 /fancybox/2.1.5/jquery.fancybox.min.css" media="screen"> 
      {# This file provides the default styling for the KeystoneJS 
        Content Editor #} 
      {%- if user and user.canAccessKeystone -%} 
        <link href="/keystone/styles/content/editor.min.css" 
          rel="stylesheet"> 
      {%- endif -%}

  1. 现在让我们将以下代码行添加到模板/主题/newBlog/layouts/default.swig 底部的脚本中:
      {# Add scripts that are globally required by your site here. #} 
      <script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/2.1.5
        /jquery.fancybox.min.js"></script> 
      <script> 
      $(document).ready(function(){ 
        // Gallery 
      $(".fancybox").fancybox({ 
          openEffect: "elastic", 
          closeEffect: "elastic" 
      }); 
      // Floating label headings for the contact form 
        $("body").on("input propertychange", ".floating-label-
          form-group", function(e) { 
          $(this).toggleClass("floating-label-form-group-with-value",
            !!$(e.target).val()); 
          }).on("focus", ".floating-label-form-group", function() { 
            $(this).addClass("floating-label-form-group-with-focus"); 
            }).on("blur", ".floating-label-form-group", function() { 
              $(this).removeClass("floating-label-form-group-
                with-focus"); 
              }); 
      }); 
      </script> 

      {# Include template-specific javascript files by extending 
        the js block #} 
     {%- block js -%}{%- endblock -%}

由于我们已经在项目中使用了 jQuery,因为 Bootstrap 依赖于它,所以我们不需要再次插入它。

  1. 打开您的终端/Shell 并输入以下命令:
gulp 

  1. 转到 http://localhost:3000/gallery,您可以看到以下结果:添加 Gallery 脚本

模板图库

请注意,我们已经将示例内容包含到我们的博客中,但不用担心;在本章的后面,我们将看到如何包含内容。

扩展 keystone.js 核心

现在我们几乎准备好了新主题。

我们现在将看到如何扩展核心 keystone.js 并在我们的博客上添加另一页,如上一个截图所示,我们有一个关于菜单项,所以让我们创建它:

  1. 在 models/folder 中创建一个名为 About.js 的新文件,并添加以下代码行:
      var keystone = require('keystone'); 
      var Types = keystone.Field.Types; 

      /** 
       * About Model 
       * ========== 
      */ 

      var About = new keystone.List('About', { 
        // Using map to show title instead ObjectID on Admin Interface 
        map: { name: 'title' }, 
        autokey: { path: 'slug', from: 'title', unique: true }, 
      }); 

      About.add({ 
        title: { type: String, initial: true, default: '',
          required: true }, description: { type: Types.Textarea } 
      }); 

      About.register();

  1. 将新模块添加到管理导航中,打开根文件夹中的 keystone.js,并添加以下突出显示的代码行:
      // Configure the navigation bar in Keystone's Admin UI
      keystone.set('nav', { 
        posts: ['posts', 'post-categories'], 
        galleries: 'galleries', 
        enquiries: 'enquiries', 
        userAdmins: 'user-admins', 
        abouts: 'abouts' 
      });

请注意,左侧的单词将显示在导航栏上作为关于菜单项,右侧的单词是 about.js 集合。

  1. 让我们自定义列显示。在 About.js 文件的 register()函数之前添加以下代码行:
 About.defaultColumns = 'title, description|60%'; 

  1. 要将路由添加到关于页面,打开 routes/index.js 并添加以下突出显示的代码行:
      // Setup Route Bindings 
      exports = module.exports = function (app) { 
         // Views 
         app.get('/', routes.views.index); 
         app.get('/about', routes.views.about); 
         app.get('/blog/:category?', routes.views.blog); 
         app.get('/blog/post/:post', routes.views.post); 
         app.get('/gallery', routes.views.gallery); 
         app.all('/contact', routes.views.contact); 

        // NOTE: To protect a route so that only admins can see it,
        use the requireUser middleware: 
        // app.get('/protected', middleware.requireUser, 
        routes.views.protected); 
      }; 

现在让我们为 routes.views.blog 函数创建控制器。

  1. 在 routes/views/文件夹中创建一个名为 about.js 的新文件,并添加以下代码:
      var keystone = require('keystone'); 
      exports = module.exports = function (req, res) { 
        var view = new keystone.View(req, res); 
        var locals = res.locals; 

         // locals.section is used to set the currently selected 
         // item in the header navigation. 
         locals.section = 'about'; 
         // Add code to show posts on index 
         locals.data = { 
           abouts: [] 
         }; 
         view.on('init', function(next) { 
           var q = keystone.list('About').model.find() 
               .limit('1'); 
             q.exec(function(err, results) { 
               locals.data.abouts = results; 
                 next(err); 
           }); 
         }); 
         // Render the view 
         view.render('about'); 
         };

  1. 在 routes/middleware.js 上添加路由,如下突出显示的代码:
      exports.initLocals = function (req, res, next) { 
        res.locals.navLinks = [ 
          { label: 'Home', key: 'home', href: '/' }, 
          { label: 'About', key: 'about', href: '/about' }, 
          { label: 'Blog', key: 'blog', href: '/blog' }, 
          { label: 'Gallery', key: 'gallery', href: '/gallery' }, 
          { label: 'Contact', key: 'contact', href: '/contact' }, 
        ]; 
        res.locals.user = req.user; 
          next(); 
      }; 

在这个例子中,我们看到如何通过使用内置函数来扩展框架的功能。

提示

您可以在此链接中阅读有关Keystone API的更多信息:github.com/keystonejs/keystone/wiki/Keystone-API

因此,所有这些步骤的最终结果将如下截图所示:

扩展 keystone.js 核心

带有关于菜单项的 Keystone 控制面板

请注意,我们可以在上一个截图中看到关于菜单。

使用控制面板插入内容

经过所有这些步骤,我们成功为我们的博客创建了一个完全定制的布局;现在我们将使用书籍源代码下载中的 sample-images 文件夹中的可用图像输入内容:

  1. 转到 http://localhost:3000/keystone,使用用户:john@doe.com 和密码:123456 访问控制面板。

  2. 转到 http://localhost:3000/keystone/post-categories,单击帖子类别链接。

  3. 单击创建帖子类别按钮,将旧车标题插入输入字段,并单击创建按钮。

  4. 对于书籍示例,我们将只使用一个类别,但在实际应用中,您可以创建任意多个。

  5. 转到 http://localhost:3000/keystone/posts,单击创建帖子按钮,并按照以下截图中显示的内容添加内容:使用控制面板插入内容

创建帖子屏幕上的示例内容

  1. 对于第二个帖子条目,重复步骤 4的相同过程,并将标题更改为不带图像的示例帖子示例 II

  2. 对于第三个帖子条目,重复步骤 4的相同过程,并将标题更改为带图像的示例帖子示例,单击上传图像按钮,并使用 sample-images 文件夹中的文件 sample-blog-image.png。

提示

请注意,您可以随时从 Packt Publishing 网站或直接从 GitHub 书库下载书籍源代码和图像样本。

步骤 6结束时,我们的控制面板将如下截图所示:

使用控制面板插入内容

帖子控制面板

正如我们所看到的,Keystone.js 具有非常简单和易于使用的界面。我们可以扩展框架的所有功能,以创建令人难以置信的东西。

我们的帖子页面如下:

使用控制面板插入内容

博客页面截图

总结

在本章中,我们讨论了关于 Keystone 框架的一些非常重要的概念,以便使用数据库创建应用程序和网站。

我们看到了如何通过使用内部 Keystone API 来创建新的模型、视图和模板来扩展框架。

此外,我们展示了使用样式表来自定义 CMS 的两种不同方式,以及如何完全改变页面结构以及如何插入新功能,比如Fancybox插件到图片库中。

在下一章中,我们将看到如何使用命令行界面(CLI)来进行 JSLint、Concat、Minify 和其他任务,只使用 Node Package Manager(NPM)来构建和部署应用程序。

第九章:使用 Node.js 和 NPM 构建前端流程

正如我们在之前的章节中提到的,我们可以使用 Node Package Manager(NPM)的工具来替换 Gulp 和Grunt任务管理器,这是处理前端依赖关系的最流行的工具。我们可以结合使用这两种工具,但在本章中我们将只探索 NPM 和一些命令,这些命令将帮助我们创建我们的应用程序。

我们将创建构建任务来 lint、连接和压缩 Javascript 文件,优化图像,编译 SASS 样式表,并通过命令行将应用程序部署到云端服务器。此外,对于这个示例,我们将使用 Loopback.io 框架来创建具有 MongoDB 作为数据库的应用程序示例。

在本章中,我们将涵盖:

  • 如何仅使用 Loopback.io CLI 创建应用程序

  • 如何安装 eslint、imagemin 和 browserify

  • 如何创建任务来 lint 错误、连接 JS 文件和优化图像

  • 如何处理 SASS 导入和编译

  • 如何使用 Heroku 工具包将应用程序部署到 Heroku

我们正在构建什么

在本章中,我们将构建一个简单的画廊应用程序,与第四章非常相似,但这次我们将使用 Loopback.io 框架的 Restful API。我们将看到如何使用 NPM 命令行创建构建任务,最终结果将与以下截图非常相似:

我们正在构建什么

主屏幕

创建基线应用程序

尽管我们已经使用了 Loopback 框架,我们强烈建议您再次安装它,以确保您的机器上有最新版本:

 npm install -g loopback

在这个示例中,我们不会对生成的代码进行太多更改,因为我们的重点是创建构建任务,但我们将使用 Loopback 框架的一些有趣特性,使用命令行:

  1. 打开终端/ shell,并输入以下命令:
 slc loopback

  1. 将应用程序命名为 chapter-09。

  2. 选择 empty-server(一个没有配置模型或数据源的空 LoopBack API),然后按 Enter。

现在我们已经创建了应用程序脚手架。不用担心终端输出建议的下一个命令,因为我们将在本书的后面讨论这些命令。

提示

您可以在此链接阅读有关Loopback CLI的更多信息:docs.strongloop.com/display/public/LB/Command-line+reference

向项目添加数据源

在我们像在第六章中所做的那样创建模型之前,这次我们将首先添加数据源。这是因为我们使用命令行来创建整个项目。这意味着我们不手动编辑任何文件。

当我们使用命令行时,一个好的做法是先创建数据源,而不是应用程序模型。这个过程可以防止需要手动编辑文件来连接模型和数据源应用程序:

  1. 在终端/ shell 中,转到 chapter-09 文件夹,并输入以下命令:
 slc loopback:datasource

  1. 按照以下截图中显示的问题进行填写:向项目添加数据源

数据源设置

默认情况下,如果我们在本地使用 MongoDB,就不需要设置用户名和密码。现在不用担心这个问题,但以后我们会看到如何更改配置以部署应用程序。如果你愿意,你也可以在本地环境中添加用户名和密码。

创建应用程序模型

现在让我们创建应用程序模型;对于这个示例,我们使用了两个模型:

  1. 在 chapter-09 文件夹中打开终端/ shell,并输入以下命令:
 slc loopback:model

使用模型名称 gallery。

  1. 按照以下截图中显示的问题填写:创建应用程序模型

画廊模型设置

在第二个属性之后,Enter**完成模型创建。

  1. 在 chapter-09 文件夹中打开终端/Shell,并输入以下命令:
 slc loopback:model

使用模型名称自行车。

  1. 按照以下截图中显示的问题填写:创建应用程序模型

自行车模型设置

在第三个属性之后,Enter**完成模型创建。

提示

您可以在此链接找到有关模型创建的更多信息:docs.strongloop.com/display/public/LB/Model+generator

现在不要担心模型之间的关系,我们将在下一步中看到,只使用命令行。

在应用程序模型之间添加关系

让我们定义模型之间的关系;我们将使用两种类型的关系,即:

  • hasmany:一个画廊可以有很多辆自行车

  • belongsTo:一辆自行车可以有一个画廊

请记住,我们只是试图做一些有用的事情,而不是复杂的事情,以说明使用 NPM 的构建过程,请按照以下步骤进行:

  1. 在 chapter-09 文件夹中打开终端/Shell,并输入以下命令:
slc loopback:relation

  1. 选择自行车模型,并按照以下问题填写:在应用程序模型之间添加关系

自行车模型关系

  1. 选择画廊模型,并使用以下信息填写问题:在应用程序模型之间添加关系

画廊模型关系

所以让我们检查一下是否一切都写得正确。

  1. 打开 common/models/gallery.json 文件,您将看到以下突出显示的代码:
    { 
      "name": "gallery", 
      "plural": "galleries", 
      "base": "PersistedModel", 
      "idInjection": true, 
      "options": { 
        "validateUpsert": true 
      }, 
      "properties": { 
          ... 
      }, 
      "validations": [], 
      "relations": { 
        "bikes": { 
          "type": "hasMany", 
          "model": "bike", 
          "foreignKey": ""
} 
      }, 
      "acls": [], 
      "methods": {} 
    } 

  1. 打开 common/models/bike.json 文件,您将看到以下突出显示的代码:
    { 
      "name": "bike", 
      "base": "PersistedModel", 
      "idInjection": true, 
      "options": { 
        "validateUpsert": true 
      }, 
      "properties": { 
        ... 
      }, 
      "validations": [], 
      "relations": { 
        "gallery": { 
          "type": "belongsTo", 
          "model": "gallery", 
          "foreignKey": "" 
        } 
      }, 
      "acls": [], 
      "methods": {} 
    } 

提示

您可以在此链接找到有关关系生成器的更多信息:docs.strongloop.com/display/public/LB/Relation+generator

只使用三个命令,我们就成功创建了示例应用程序的基础。下一步是在 client 文件夹中创建一个静态网站。

设置静态站点

就像我们在第六章中所做的那样,使用 Restful API 和 Loopback.io 构建客户反馈应用程序,让我们将 client 文件夹设置为静态站点:

  1. 将 server/boot/root.js 文件重命名为 server/boot/_root.js。

  2. 将以下突出显示的行添加到 server/middleware.json 中:

      { 
        "initial:before": { 
          "loopback#favicon": {} 
      }, 
        "initial": { 
          "compression": {}, 
          "cors": { 
            "params": { 
              "origin": true, 
              "credentials": true, 
              "maxAge": 86400 
            } 
          }, 
          "helmet#xssFilter": {}, 
          "helmet#frameguard": { 
            "params": [ 
              "deny" 
            ] 
          }, 
          "helmet#hsts": { 
            "params": { 
              "maxAge": 0, 
              "includeSubdomains": true 
            } 
          }, 
          "helmet#hidePoweredBy": {}, 
          "helmet#ieNoOpen": {}, 
          "helmet#noSniff": {}, 
          "helmet#noCache": { 
            "enabled": false 
          } 
        }, 
        "session": {}, 
        "auth": {}, 
        "parse": {}, 
        "routes": { 
          "loopback#rest": { 
            "paths": [ 
              "${restApiRoot}" 
            ] 
          } 
        }, 
        "files": { 
          "loopback#static": { 
            "params": "$!../client" 
          } 
        }, 
        "final": { 
          "loopback#urlNotFound": {} 
        }, 
        "final:after": { 
          "loopback#errorHandler": {} 
        } 
      }

  1. 在./client 文件夹中,创建一个名为 index.html 的新文件,并添加以下内容:
      <!DOCTYPE html> 
      <html> 
        <head><title>Bikes Gallery</title></head> 
        <body> 
          <h1>Hello Node 6 Blueprints!</h1> 
        </body> 
      </html> 

现在是时候检查之前的更改,并在浏览器中查看最终结果了。

  1. 打开终端/Shell 并输入以下命令:
 npm start

  1. 打开您喜欢的浏览器,转到 http://localhost:3000/。

您应该会看到Hello Node 6 Blueprints!消息。

我们还在 http://localhost:3000/api/bikeshttp://localhost:3000/api/galleries 上有 Restful API。

现在我们将看到如何重组一些目录,以准备使用 NPM 构建任务在云中部署应用程序。

重构应用程序文件夹

我们的重构过程包括两个步骤。

首先,让我们为应用程序源文件创建一个目录,例如 JavaScript、SCSS 和图像文件。

在第二步中,我们将在 client 文件夹中创建一些目录来接收我们的脚本。

让我们为图像、库、脚本和 SCSS 文件创建源文件夹。

创建图像文件夹

在这个文件夹中,我们将在使用 imagemin-cli 进行优化技术处理之前存储图像。

  1. 在根项目内,创建一个名为 src 的文件夹。

  2. 在 src 文件夹中,创建一个名为 images 的文件夹。

  3. 在图像文件夹中,创建一个名为 gallery 的文件夹。

  4. 从 Packt 网站(www.packtpub.com)或 GitHub 上的官方书籍存储库下载第九章的示例图像文件,然后将图像粘贴到 gallery 文件夹中。

提示

您可以在此链接了解更多有关 imagemin cli 的信息:github.com/imagemin/imagemin-cli

创建 libraries 文件夹

libraries 文件夹将存储一些 jQuery 插件。在 src 文件夹内,创建一个名为 libs 的文件夹。

创建 scripts 文件夹

由于我们使用了 jQuery 和一些插件,我们需要编写一些代码来使用 jQuery 库;我们将使用这个文件夹来做到这一点:

  1. 在 src 文件夹内,创建一个名为 scripts 的文件夹。

  2. 在 src/scripts 文件夹内,创建一个名为 gallery.js 的文件,并添加以下代码:

      (function (){ 
          'use-strict' 
         //jQuery fancybox activation 
         $('.fancybox').fancybox({ 
              padding : 0, 
             openEffect  : 'elastic' 
         }); 
      })(); 

在这个例子中,我们只使用了一个插件,但在大型应用程序中,使用多个插件是非常常见的;在这种情况下,我们会为每个功能有一个文件。

然后,为了提高应用程序的性能,我们建议将所有脚本合并成一个文件。

创建 SASS 文件夹

SASS 文件夹将存储 scss 文件。我们正在使用 Bootstrap 框架,对于这个例子,我们将使用 SASS 分离版本来设置 Bootstrap 框架;现在不用担心这个,因为在本章后面我们会看到如何获取这些文件:

  1. 在 src 文件夹内,创建一个名为 scss 的文件夹。

  2. 在 scss 文件夹内,创建一个名为 vendor 的文件夹。

安装 Bower

正如我们在之前的章节中看到的,我们将使用 Bower 来管理前端依赖关系:

  1. 打开终端/Shell 并输入以下命令:
npm install bower -g

  1. 创建一个名为.bowerrc 的文件并将其保存在根文件夹中。

  2. 将以下内容添加到.bowerrc 文件中:

      { 
        "directory": "src/components", 
        "json": "bower.json" 
      }

  1. 打开终端/Shell 并输入以下命令:
 bower init

  1. 按照以下截图中显示的问题填写:安装 Bower

设置 Bower.json

安装应用程序依赖关系

在这个例子中,我们只使用了一个 jQuery 插件加上 Bootstrap 框架,所以让我们首先使用 Bower CLI 来安装 Bootstrap:

  1. 打开终端/Shell 并输入以下命令:
bower install bootstrap#v4.0.0-alpha --save

只需打开 src/components 文件夹查看 Bootstrap 和 jQuery 文件夹。

  1. 现在我们将在图像库中安装 jQuery fancybox 插件。打开终端/Shell 并输入以下命令:
 bower install fancybox --save

因此,此时 src 文件夹将具有以下结构:

  • components/

  • bootstrap/

  • fancybox/

  • jquery/

创建 scss 文件夹结构

现在让我们设置 scss 文件夹来编译 bootstrap.scss 文件:

  1. 打开 src/components/bootstrap 文件夹并复制 SCSS 文件夹中的所有内容。

  2. 将内容粘贴到 src/scss/vendor 文件夹内。

  3. 在 src/文件夹内创建一个名为 main.scss 的文件,并添加以下内容:

      // Project Style 

      // Import Botstrap 
      @import "vendor/bootstrap"; 

      // 
      body { 
        padding-top: 5rem; 
      } 
      .starter-template { 
        padding: 3rem 1.5rem; 
        text-align: center; 
        @include clearfix 
      } 

许多开发人员不会以这种方式使用 Bootstrap 框架,有些人只会在项目中使用 bootstrap.css 或 bootstrap.min.css 文件。这没问题,但是当我们以这种方式使用框架时,我们可以在我们自己的样式表中使用所有框架的资源,因此我们可以在我们的样式表中使用所有的 mixin 和变量。

例如,突出显示的代码来自 Bootstrap mixins,我们可以将其应用到我们自己的样式表中:

      .starter-template { 
        padding: 3rem 1.5rem; 
        text-align: center; 
        @include clearfix 
      } 

提示

您可以在此链接了解更多有关 SASS 的信息:sass-lang.com/

重构客户端文件夹

客户端文件夹将具有一个非常基本的结构,用于存储 CSS、JavaScript 和图像文件。

在这个例子中,我们将使用最新的稳定版本的 AngularJS 来创建我们应用程序的页面:

  1. 在客户端文件夹内,创建以下文件夹:
  • css/

  • images/gallery/

  • js/

  • js/libs/

  • js/scripts/

  • views/

创建所有这些文件夹后,客户端目录将如下截图所示:

重构客户端文件夹

客户端文件夹结构

添加应用程序视图

现在是时候创建应用程序视图文件夹来存储所有应用程序模板了:

  1. 在 client/src 文件夹中,创建一个名为 home.html 的新文件,并添加以下代码:
      <div class="col-md-6" ng-repeat="item in vm.listProducts"> 
        <div class="card" > 
          <img class="card-img-top" ng-src="img/{{ item.image }}"
           alt="Card image cap" width="100%"> 
          <div class="card-block"> 
            <h4 class="card-title">{{ item.name }}</h4> 
            <p class="card-text">{{ item.description }}</p> 
            <a ui-sref="galleries({itemId:item.id})" class="btn 
             btn-secondary">View Gallery</a> 
          </div> 
        </div> 
      </div> 
      </div> 

  1. 在 client/src 文件夹中,创建一个名为 galleries.html 的新文件,并添加以下代码:
      <div class="row"> 
        <div class="col-md-4" ng-repeat="item in vm.listProducts"> 
          <div class="card" > 
            <a href="{{ item.image }}" class="fancybox" rel="gallery"
             > 
            <img class="card-img-top" ng-src="img/{{ item.image }}" alt=
             "{{ item.name }}" width="100%"/> 
            </a> 
            <div class="card-block"> 
              <h4 class="card-title">{{ item.name }}</h4> 
              <p class="card-text">{{ item.model }} - {{ item.category }}
              </p> 
            </div> 
          </div> 
        </div> 
      </div> 

  1. 打开 client/index.html 文件,并用以下内容替换代码:
      <!DOCTYPE html> 
      <html ng-app="bikesGallery"> 
        <head><title>Bikes Gallery</title></head> 
          <link rel="stylesheet" href="css/main.css"> 
          <link rel="stylesheet" href="components/fancybox/source
           /jquery.fancybox.css"> 
        <body> 
          <nav class="navbar navbar-fixed-top navbar-dark bg-inverse"> 
            <div class="container"> 
              <a class="navbar-brand" href="#">Chapter 09</a> 
                <ul class="nav navbar-nav"> 
                  <li class="nav-item active"><a class="nav-link" href="/">
                   Home <span class="sr-only">(current)</span></a>
                  </li> 
                </ul> 
            </div> 
          </nav> 

        <div class="container"> 
          <div id="title"> 
            <div class="starter-template"> 
              <h1>Image Gallery</h1> 
                <p class="lead">Select a Gallery.</p> 
             </div> 
          </div> 
        <div class="" ui-view> 

      </div> 
      </div> 
      </div> 

      <!-- Scripts at bottom --> 
      <script src='js/libs/jquery.min.js'></script> 
      <script src="img/angular.js"></script> 
      <script src="img/angular-resource.js"></script> 
      <script src="img/angular-ui-router.js"></script> 
      <script src="img/app.js"></script> 
      <script src="img/app.config.js"></script> 
      <script src="img/app.routes.js"></script> 
      <script src="img/services.js"></script> 
      <script src="img/controllers.js"></script> 
      <script src="img/libs.js"></script> 
      <script src="img/scripts.js"></script> 

      </body> 
      </html>

安装 AngularJS 文件

现在是时候安装 AngularJS 文件并创建应用程序了。在本示例中,我们将在本节后面探索 Loopback 框架的 AngularJS SDK;为此,我们选择使用 AngularJS 来构建我们的前端应用程序:

  1. 打开终端/Shell 并输入以下命令:
bower install angularjs#1.5.0 --save

  1. 打开终端/Shell 并输入以下命令:
bower install angular-resource#1.5.0 --save

  1. 打开终端/Shell 并输入以下命令:
bower install angular-ui-router --save

提示

您可以在此链接了解更多关于 AngularJS 的信息:docs.angularjs.org/api

创建 AngularJS 应用程序

最后,我们将创建 AngularJS 应用程序,所以请按照以下步骤进行:

  1. 在 client/js 文件夹中,创建一个名为 app.js 的新文件,并添加以下代码:
     (function(){ 
          'use strict'; 

          angular 
          .module('bikesGallery', ['ui.router','lbServices']); 

      })();

现在不用担心 lbServices 依赖项;在本章后面,我们将看到如何使用 Loopback 框架构建的 AngularJS SDK 工具来创建此文件。

  1. 在 client/js 文件夹中,创建一个名为 app.config.js 的新文件,并添加以下代码:
      (function(){ 
          'use strict'; 

          angular 
           .module('bikesGallery') 
           .config(configure) 
            .run(runBlock); 

          configure.$inject = ['$urlRouterProvider', '$httpProvider', 
                              '$locationProvider']; 

          function configure($urlRouterProvider, $httpProvider, 
                            $locationProvider) { 

            $locationProvider.hashPrefix('!'); 
            // This is required for Browser Sync to work poperly 
            $httpProvider.defaults.withCredentials = true; 
            $httpProvider.defaults.headers.common['X-Requested-With']
              = 'XMLHttpRequest'; 
            $urlRouterProvider 
            .otherwise('/'); 
          } 
          runBlock.$inject = ['$rootScope', '$state', '$stateParams']; 
          function runBlock($rootScope, $state, $stateParams ) { 
            $rootScope.$state = $state; 
            $rootScope.$stateParams = $stateParams; 
          } 
      })();

  1. 在 client/js 文件夹中,创建一个名为 app.routes.js 的新文件,并添加以下代码:
      (function(){ 
        'use strict'; 

          angular 
           .module('bikesGallery') 
           .config(routes); 

          routes.$inject = ['$stateProvider']; 
          function routes($stateProvider) { 
            $stateProvider 
            .state('home', { 
                url:'/', 
                templateUrl: 'views/home.html', 
                controller: 'HomeController', 
                controllerAs: 'vm' 
            }) 
            .state('galleries', { 
                url:'/galleries/{itemId}/bikes', 
                templateUrl: 'views/galleries.html', 
                controller: 'GalleryController', 
                controllerAs: 'vm' 
            }); 
          } 
      })();

  1. 在 client/js 文件夹中,创建一个名为 controllers.js 的新文件,并添加以下代码:
      (function(){ 
        'use strict'; 

          angular 
            .module('bikesGallery') 
            .controller('GalleryController', GalleryController) 
            .controller('HomeController', HomeController); 

          HomeController.$inject = ['Gallery']; 
          function HomeController(Gallery) { 
            var vm = this; 
            vm.listProducts = Gallery.find(); 
            //console.log(vm.listProducts); 
          } 

          GalleryController.$inject = ['Gallery', '$stateParams']; 
          function GalleryController(Gallery, $stateParams) { 
            var vm = this; 
            var itemId = $stateParams.itemId; 
            //console.log(itemId); 
            vm.listProducts = Gallery.bikes({ 
              id: itemId 
          }); 
          //console.log(vm.listProducts); 
          } 
      })();

使用 Loopback 的 AngularJS SDK

我们将使用 Loopback 框架的内置 AngularJS SDK 自动生成所有应用程序服务:

  1. 打开终端/Shell 并输入以下命令:
lb-ng ./server/server.js ./client/js/services.js

上述命令将在 client/js 文件夹中创建一个名为 services.js 的文件,其中包含 Loopback 框架创建的 Restful API 中的所有方法(创建、读取、更新和删除)以及其他许多方法。

提示

您可以通过在终端/Shell 中在根项目文件夹中运行 npm start 命令来检查本地 API。API 将在 http://0.0.0.0:3000/explorer 上可用。

  1. lbServices 部分有以下 CRUD 方法和许多其他方法:
      "create": { 
        url: urlBase + "/galleries", 
        method: "POST" 
      }, 
      "upsert": { 
        url: urlBase + "/galleries", 
        method: "PUT" 
      }, 
      "find": { 
        isArray: true, 
        url: urlBase + "/galleries", 
        method: "GET" 
      }, 
      "deleteById": { 
        url: urlBase + "/galleries/:id", 
        method: "DELETE" 
      },

  1. 要使用其中一种方法,我们只需要将工厂注入到 Angular 控制器中,如下面突出显示的代码所示:
      GalleryController.$inject = ['Gallery', '$stateParams']; 
      function GalleryController(Gallery, $stateParams) { 
        ... 
      } 

然后我们可以在控制器中使用这些方法,就像以下示例中所示的那样:

      Gallery.create(); 
      Gallery.find(); 
      Gallery.upsert({ id: itemId }); 
      Gallery.delete({ id: itemId }); 

这是一个简单而非常有用的服务,用于处理我们应用程序中创建的所有模型的所有端点。

应用程序的第一部分已经几乎完成,但我们仍需要添加一些内容,使其更加愉快。

让我们创建一些内容。如前所述,您可以从 Packt 网站 (www.packtpub.com) 或直接从书的 GitHub 存储库下载整个示例代码。

向应用程序添加内容

您可以通过两种方式添加内容,第一种是使用应用程序创建的端点,第二种是使用迁移文件。

在接下来的几行中,我们将展示如何使用第二种选项;这可能是一个简短而有趣的过程,用于创建迁移文件:

  1. 在 server/boot/ 文件夹中,创建一个名为 create-sample-models.js 的文件,并添加以下内容以创建一个 Gallery Model 的迁移文件:
      module.exports = function(app) { 
        // automigrate for models, everytime the app will running,
         db will be replaced with this data. 

        app.dataSources.galleryDS.automigrate('gallery', function(err)
        { 
          if (err) throw err; 
           // Simple function to create content 
          app.models.Gallery.create( 
            [ 
              { 
                "name":"Bikes", 
                "image": "images/gallery/sample-moto-gallery.jpg", 
                "link": "bikes.html", 
                "description":"Old and Classic Motorcycles", 
                "id":"5755d253b4aa192e41a6be0f" 
              },{ 
                  "name":"Cars", 
                  "image": "images/gallery/sample-car-gallery.jpg", 
                  "link": "cars.html", 
                  "description":"Old and Classic Cars", 
                  "id":"5755d261b4aa192e41a6be10" 
                } 
            ],
            function(err, galleries) { 
              if (err) throw err; 
              // Show a success msg on terminal 
              console.log('Created Motorcycle Gallery Model: \n',
               galleries); 
            }); 
          }); 

  1. 在 server/boot/ 文件夹中,添加以下内容以创建一个 Bike Model 的迁移文件:
      app.dataSources.galleryDS.automigrate('bike', function(err) { 
        if (err) throw err; 
         // Simple function to create content 
        app.models.Bike.create( 
            [ 
                { 
                  "name":"Harley Davidson", 
                  "image": "images/gallery/sample-moto1.jpg", 
                  "model":"Knucklehead", 
                  "category":"Custom Classic Vintage", 
                  "id":"5755d3afb4aa192e41a6be11", 
                  "galleryId":"5755d253b4aa192e41a6be0f" 
                },{ 
                    "name":"Harley Davidson", 
                    "image": "images/gallery/sample-moto2.jpg", 
                    "model":"Rare Classic", 
                    "category":"Custom Classic Vintage", 
                    "id":"5755d3e8b4aa192e41a6be12", 
                    "galleryId":"5755d253b4aa192e41a6be0f" 
                },{ 
                    "name":"Old Unknown Custom Bike", 
                    "image": "images/gallery/sample-moto3.jpg", 
                    "model":"Custom", 
                    "category":"Chopper", 
                    "id":"5755d431b4aa192e41a6be13", 
                    "galleryId":"5755d253b4aa192e41a6be0f" 
                },{ 
                    "name":"Shadow Macchit", 
                    "image": "images/gallery/sample-car1.jpg", 
                    "model":"Classic", 
                    "category":"Old Vintage", 
                    "id":"5755d43eb4aa192e41a6be14", 
                    "galleryId":"5755d261b4aa192e41a6be10" 
                },{ 
                    "name":"Buicks", 
                    "image": "images/gallery/sample-car2.jpg", 
                    "model":"Classic", 
                    "category":"Classic", 
                    "id":"5755d476b4aa192e41a6be15", 
                    "galleryId":"5755d261b4aa192e41a6be10" 
                },{ 
                    "name":"Ford", 
                    "image": "images/gallery/sample-car3.jpg", 
                    "model":"Corsa", 
                    "category":"Hatch", 
                    "id":"5755d485b4aa192e41a6be16", 
                    "galleryId":"5755d261b4aa192e41a6be10" 
                } 

               ], function(err, bikes) { 
                    if (err) throw err; 
                     // Show a success msg on terminal 
                    console.log('Created Bike Model: \n', bikes); 
                 }); 
               }); 
      };

提示

不要忘记在第一次部署到 Heroku 后删除此文件。

创建构建任务

现在是时候只使用 NPM 来创建我们的任务了。

在开始之前,重要的是要记住 NPM 有两个特殊的命令,可以直接调用,即 start 和 test。因此,我们将使用 run 命令来运行我们创建的所有其他任务。

我们在本节的目标是:

  • 从源目录复制一些文件到客户端目录

  • 验证 JavaScript 文件中的错误

  • 从 src/scss 编译 SASS 文件并将其保存在 client/css 文件夹中

  • 优化从 src/images/gallery 到 client/images/gallery 的图像

  • 将 JavaScript 文件从 src/scripts 连接到 client/js/scripts

安装依赖项

为了完成这些任务,我们需要安装一些命令行界面(CLI)工具:

  1. 打开终端/Shell 并输入以下命令:
 npm install copy-cli --save-dev
 npm install -g eslint
 npm install eslint --save-dev
 npm install -g node-sass
 npm install browserify --save-dev
 npm intall -g imagemin-cli
 npm install -g imagemin

在本示例中,我们的目的是展示如何使用构建工具,因此我们不会深入研究每个工具。

但在我们继续之前,让我们设置 JavaScript 验证器 eslint。

提示

您可以在此链接阅读有关 eslint 的更多信息:eslint.org/

  1. 在根项目内,创建一个名为.eslintrc.json 的文件并添加以下代码:
      { 
        "env": { 
          "browser": true 
        }, 
        "globals": { 
          "angular": 1, 
          "module": 1, 
          "exports": 1 
        }, 
        "extends": "eslint:recommended", 
        "rules": { 
          "linebreak-style": [ 
            "error", 
            "unix" 
          ], 
          "no-mixed-spaces-and-tabs": 0, 
          "quotes": 0, 
          "semi": 0, 
          "comma-dangle": 1, 
          "no-console": 0 
        } 
      } 

提示

您可以在此链接阅读有关 Eslint 规则的更多信息:eslint.org/docs/rules/

创建复制任务

我们将在将其插入到我们的 package.json 文件之前创建每个任务;这样更容易理解每个过程。

复制任务将如下所示:

  • 复制 jQuery 文件

  • 复制 AngularJS 主库

  • 复制 AngularJS 资源库

  • 复制 AngularJS ui-router 库

因此,我们需要将这些文件(jQuery 和 AngularJS)从源文件夹复制到客户端文件夹:

"copy-jquery": "copy ./src/components/jquery/dist/jquery.js > 
  ./client/js/libs/jquery.js", 
"copy-angular": "copy ./src/components/angular/angular.js > 
  ./client/js/libs/angular.js", 
"copy-angular-resource": "copy ./src/components/angular-resource/angular-resource.js > 
  ./client/js/libs/angular-resource.js", 
"copy-angular-ui-router": "copy ./src/components/angular-ui-router/release/angular-ui-router.js > 
  ./client/js/libs/angular-ui-router.js", 

最后的复制任务将执行所有其他复制任务:

"copy-angular-files": "npm run copy-angular && npm run copy-angular-resource && npm run copy-angular-ui-router", 

目前不用担心运行复制任务;在本章后面,我们将在部署之前逐个执行它们。

创建 SASS 任务

SASS 任务将非常简单,我们只需编译 scss 文件并将它们插入到 client/css 文件夹中:

"build-css": "node-sass --include-path scss src/scss/main.scss   client/css/main.css", 

创建 linting 任务

我们将使用.eslintrc.json 配置应用于 client/js 文件夹中的所有 JavaScript 文件:

"lint-js": "eslint client/js/*.js --no-ignore", 

创建图像优化任务

在任何 Web 应用程序中,另一个重要的任务是优化所有图像文件,出于性能原因:

"imagemin": "imagemin src/images/gallery/* --o client/images/gallery", 

创建连接任务

concat 任务将连接 libs 和 scripts 中的所有脚本文件:

"concat-js-plugins": "browserify src/libs/*.js -o client/js/libs/libs.js", 
"concat-js-scripts": "browserify src/scripts/*.js -o client/js/scripts/scripts.js", 

最后的 concat 任务执行所有其他 concat 任务:

"prepare-js": "npm run concat-js-plugins && npm run concat-js-scripts" 

创建构建任务

构建任务只是在单个任务中执行之前的每个步骤:

"build": "npm run lint-js && npm run copy-angular-files && npm run build-css && npm run prepare-js && npm run imagemin" 

现在让我们将所有任务添加到 package.json 文件中。打开 package.json 文件并添加以下突出显示的代码:

  { 
    "name": "chapter-09", 
    "version": "1.0.0", 
    "main": "server/server.js", 
    "scripts": { 
      "start": "node .", 
      "pretest": "eslint .", 
      "posttest": "nsp check", 
      "copy-jquery": "copy ./src/components/jquery/dist/jquery.js > 
        ./client/js/libs/jquery.js", 
      "copy-angular": "copy ./src/components/angular/angular.js >
        ./client/js/libs/angular.js", 
      "copy-angular-resource": "copy ./src/components/angular-resource
      /angular-resource.js >
        ./client/js/libs/angular-resource.js", 
      "copy-angular-ui-router": "copy ./src/components/angular-ui-router
      /release/angular-ui-router.js >
        ./client/js/libs/angular-ui-router.js", 
      "copy-angular-files": "npm run copy-angular && npm run copy-angular-
      resource && npm run copy-angular-ui-router", 
      "build-css": "node-sass --include-path scss src/scss/main.scss 
      client/css/main.css", "lint-js": "eslint client/js/*.js --no-ignore",
      "imagemin": "imagemin src/images/gallery/* --o client/images/gallery", 
      "concat-angular-js": "browserify ./src/libs/angular.js ./src/libs/
      angular-resource.js ./src/libs/angular-ui-router.js > client/js
      /libs/libs.js", "concat-js-plugins": "browserify src/libs/*.js -o 
      client/js/libs/libs.js", "concat-js-scripts": "browserify 
      src/scripts/*.js -o client/js/scripts/scripts.js", "prepare-js": 
      "npm run concat-js-plugins && npm run concat-js-scripts", "build":
      "npm run lint-js && npm run copy-angular-files && npm run build-css && 
      npm run prepare-js && npm run imagemin" 
    }, 
    "dependencies": { 
         ... 
    }, 
    "devDependencies": { 
         ... 
    }, 
    "repository": { 
         ... 
    }, 
    "license": "MIT", 
    "description": "chapter-09", 
    "engines": { 
      "node": "5.0.x" 
    } 
  } 

使用单独命令的任务

正如我们之前提到的,我们可以单独执行我们创建的每个任务。

例如,要优化图像文件,我们只需运行 imagemin 任务。只需打开终端/Shell 并输入以下命令:

 npm run imagemin

终端上的输出将如下所示:

 8 images minified

我们可以为每个任务执行相同的操作。

部署到 Heroku 云

部署我们的应用程序的第一步是在Heroku 云服务上创建一个免费帐户:

  1. 转到signup.heroku.com/并创建一个免费帐户。

  2. toolbelt.heroku.com/上为您的平台下载Heroku 工具包

  3. 按照您的平台进行安装过程。

现在您必须在您的机器上安装 Heroku 工具包,以进行测试。

  1. 打开终端/Shell 并输入以下命令:
 heroku --help

终端输出列出了使用Heroku CLI的所有可能操作。

提示

本书假设您的机器上已经安装了 git 源代码控制;如果您还没有安装,请查看此页面:git-scm.com/downloads

创建 Heroku 应用程序

现在我们将创建一个应用程序并将其发送到您新创建的 Heroku 帐户:

  1. 创建一个名为.Procfile 的文件并将其保存在根项目文件夹中。

  2. 将以下代码粘贴到.Procfile 文件中:

      web: slc run 

  1. 打开终端/Shell 并输入以下命令:
 git init

上一个命令初始化了一个 git 存储库。

  1. git add 命令将所有文件添加到版本跟踪:
 git add

  1. git commit 命令将所有文件发送到本地机器上的版本控制。
 git commit -m "initial commit"

现在是时候登录您的 Heroku 帐户并将所有项目文件发送到 Heroku git 源代码控制。

  1. 打开终端/ shell 并输入以下命令:
 heroku login

输入您的用户名和密码。

  1. 打开终端/ shell 并输入以下命令:
heroku apps:create --buildpack https://github.com/strongloop
     /strongloop-buildpacks.git

上一个命令将使用 strongloop-buildpacks 来配置和部署 Loopback 应用程序。

创建一个 deploy.sh 文件

最后,我们将使用.sh 文件创建我们的部署任务:

  1. 在根文件夹中创建一个名为 bin 的文件夹。

  2. 在 bin 文件夹内,创建一个名为 deploy.sh 的文件。

  3. 将以下代码添加到 bin/deploy.sh 文件的末尾:

      #!/bin/bash 

      set -o errexit # Exit on error 

      npm run build # Generate the bundled Javascript and CSS 

      git push heroku master # Deploy to Heroku 

  1. 在 package.json 文件的所有任务的末尾添加以下代码行:
      "scripts": {
        ... 
        "deploy": "./bin/deploy.sh" 
      }

现在,每当您进行一次提交并输入 npm run deploy 命令进行一次更改时,引擎将启动 deploy.sh 文件并将所有已提交的更改上传到 Heroku 云服务。

  1. 打开终端/ shell 并输入以下命令:
 npm run deploy

  1. 如果您遇到权限错误,请执行以下操作。在 bin 文件夹内打开终端/ shell,并输入以下命令:
 chmod 755 deploy.sh

默认情况下,Heroku 云服务将为您的应用程序创建一个 URL,就像这样:

https://some-name-1234.herokuapp.com/。

在终端输出的末尾,您将看到类似以下行的内容:

remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 79.7M
remote: -----> Launching...
remote:        Released v13
remote:        https://yourURL-some-23873.herokuapp.com/
                   deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/yourURL-some-23873.git

最终结果将是我们的示例应用程序部署到 Heroku 云服务。

只需转到 https://yourURL-some-23873.herokuapp.com/,您将看到以下结果:

创建一个 deploy.sh 文件

Heroku 云服务上的应用程序

当您单击“自行车查看画廊”按钮时,您将看到自行车画廊,如下所示:

创建一个 deploy.sh 文件

自行车画廊

此外,当您单击每辆自行车时,您将看到 fancybox 插件在起作用。

总结

在本章中,我们将更深入地探索 Loopback 框架及其命令行界面。

我们还看到了如何使用 Loopback AngularJS SDK 配置 AngularJS 应用程序,为应用程序的每个端点创建所有服务。

然后,我们使用 NPM 作为单一的构建工具来探索这些设施。

我们还介绍了如何在 Heroku 上创建和设置帐户,以及如何通过集成 Loopback、Git 和 Heroku 工具包来自动部署我们的应用程序。

在下一章中,我们将看到如何使用容器的概念来运行 Node.js 应用程序。

第十章:使用持续集成和 Docker 创建和部署

在本章中,我们将探索使用 Node.js 应用程序的持续交付开发过程。

在之前的章节中,我们看到了许多使用 Node.js 和 Express、Loopback 等框架开发应用程序的方法,包括使用不同的数据库,如 MongoDB 和 MySql,以及一些用于用户身份验证、会话、cookie 等的中间件。

在第九章中,使用 Node.js 和 NPM 构建前端流程,我们看到了如何使用命令行部署应用程序,以及如何使用几个命令直接将项目上传到云端。

在本章中,我们将看到如何将一些更多的工具集成到我们的开发环境中,以处理单元测试和自动部署,如何设置环境变量来保护我们的数据库凭据,并且如何使用 Docker 容器的概念创建一个完整的应用程序。

在本章中,我们将涵盖以下主题:

  • 如何处理 CI 解决方案

  • 如何测试 Node.js 应用程序

  • 如何配置 MongoDB 云实例和环境变量

  • 如何在构建和测试过程中集成 GitHub、Heroku 和 Codeship

  • 如何创建 Docker 镜像以及如何使用 Docker 容器

我们正在构建什么

在本章中,我们将使用 Express 框架构建一个应用程序,使用了在之前章节中已经使用过的一些技术,比如使用 Passport 中间件进行用户会话和用户身份验证,使用电子邮件和密码。我们还将使用 MongoDB、Mongoose 和 Swig 模板。

结果将如下截图所示:

我们正在构建什么

主屏幕截图

持续集成的含义

持续集成CI)的工作流程通常包括四个步骤。我们将用图表和简要描述来说明所有四个阶段。

以下图表显示了 CI 解决方案的工作原理:

持续集成的含义

持续集成过程

  1. 将代码提交到存储库。

  2. CI 界面构建应用程序。

  3. 执行测试。

  4. 如果所有测试都成功,代码将被部署。

创建基线应用程序

让我们开始构建应用程序。首先,我们将创建一个应用程序文件夹,并添加一些根文件,比如.gitignore、package.json、.env 等等。

添加根文件

  1. 创建一个名为 chapter-10 的文件夹。

  2. 在 chapter-10 文件夹中,创建一个名为 package.json 的新文件,并添加以下代码:

      { 
        "name": "chapter-10", 
        "version": "1.0.0", 
        "main": "server.js", 
        "description": "Create an app for the cloud with Docker", 
        "scripts": { 
          "build": "npm-run-all build-*", 
          "build-css": "node-sass public/css/main.scss >
           public/css/main.css", 
          "postinstall": "npm run build", 
          "start": "node server.js", 
          "test": "mocha", 
          "watch": "npm-run-all --parallel watch:*", 
          "watch:css": "nodemon -e scss -w public/css -x npm run
           build:css" 
        }, 
        "dependencies": { 
          "async": "¹.5.2", 
          "bcrypt-nodejs": "⁰.0.3", 
          "body-parser": "¹.15.1", 
          "compression": "¹.6.2", 
          "dotenv": "².0.0", 
          "express": "⁴.13.4", 
          "express-flash": "0.0.2", 
          "express-handlebars": "³.0.0", 
          "express-session": "¹.2.1", 
          "express-validator": "².20.4", 
          "method-override": "².3.5", 
          "mongoose": "⁴.4.8", 
          "morgan": "¹.7.0", 
          "node-sass": "³.6.0", 
          "nodemon": "¹.9.1", 
          "npm-run-all": "¹.8.0", 
          "passport": "⁰.3.2", 
          "passport-local": "¹.0.0", 
          "swig": "¹.4.2" 
        }, 
        "devDependencies": 
          "mocha": "².4.5", 
          "supertest": "¹.2.0" 
        }, 
        "engines": { 
          "node": "6.1.0" 
        } 
      }

提示

请注意,在 devDependencies 中,我们将使用一些模块来为我们的应用程序编写测试。我们将在本书的后面看到如何做到这一点。

  1. 创建一个名为.env 的文件,并添加以下代码:
      SESSION_SECRET='<SESSION_SECRET>' 
      #MONGODB='<>' 
      MONGODB='<MONGODB>' 

不要担心之前的代码;我们将在本章后面使用环境变量在HerokuCodeship上替换这段代码,并且我们还将配置此文件以使用 Docker 容器。

提示

出于安全原因,如果您正在进行商业项目,请不要将您的凭据上传到开源存储库;即使您有一个私人存储库,也建议您在生产中使用环境变量。

  1. 创建一个名为 Profile 的文件,并添加以下代码:
      web: node server.js 

正如我们在之前的章节中所看到的,这个文件负责使我们的应用程序在 Heroku 上运行。即使它不是强制性的,将它包含进来也是一个良好的做法。

另外,由于我们正在使用 git 源代码控制,将.gitignore 文件包含进来是一个良好的做法。

  1. 创建一个名为.gitignore 的文件,并添加以下代码:
      lib-cov 
      *.seed 
      *.log 
      *.csv 
      *.dat 
      *.out 
      *.pid 
      *.gz 
      *.swp 

      pids 
      logs 
      results 
      tmp 
      coverage 

      # API keys 
      .env 

      # Dependency directory 
      node_modules 
      bower_components 
      npm-debug.log 

      # Editors 
      .idea 
      *.iml 

      # OS metadata 
      .DS_Store 
      Thumbs.db 

注意,目前我们将.env 文件保留在 gitignore 文件之外;在本书的后面,我们将取消跟踪此文件。

创建 config 文件夹和文件

通常,所有 Node.js 应用程序都使用一个名为 Config 的文件夹,用于存储所有应用程序配置文件。所以让我们创建一个。

  1. 在项目根目录下,创建一个名为 config 的新文件夹。

  2. 创建一个名为 passport.js 的文件,并添加以下代码:

      // load passport module 
      var passport = require('passport'); 
      var LocalStrategy = require('passport-local').Strategy; 
      // load up the user model 
      var User = require('../models/User'); 

      passport.serializeUser(function(user, done) { 
        // serialize the user for the session 
        done(null, user.id); 
      }); 

      passport.deserializeUser(function(id, done) { 
        // deserialize the user 
        User.findById(id, function(err, user) { 
          done(err, user); 
        }); 
      }); 

      // using local strategy 
      passport.use(new LocalStrategy({ usernameField: 'email' }, 
       function(email, password, done) { 

        User.findOne({ email: email }, function(err, user) { 
          if (!user) { 
            // check errors and bring the messages 
            return done(null, false, { msg: 'The email: ' + email +
             ' is already taken. '}); 
          } 
          user.comparePassword(password, function(err, isMatch) { 
            if (!isMatch) { 
              // check errors and bring the messages 
              return done(null, false, { msg:'Invalid email or
              password'});   
            } 
            return done(null, user); 
          }); 
        }); 
      }));

前面的代码将使用Flask中间件来处理用户身份验证的错误消息,就像我们在第一章中看到的那样,使用 MVC 设计模式构建类似 Twitter 的应用程序

创建控制器文件夹和文件

由于我们正在构建一个简单的应用程序,我们只会有两个控制器,一个用于用户,另一个用于主页:

  1. 在根项目文件夹内,创建一个名为 controllers 的新文件夹。

  2. 创建一个名为 home.js 的文件,并添加以下代码:

      // Render Home Page 
      exports.index = function(req, res) { 
        res.render('home', { 
          title: 'Home' 
        }); 
      };

现在让我们添加所有与用户相关的功能,比如登录、注册、授权、账户和登出。我们将在之前的每个功能后添加一个功能。

添加模块和身份验证中间件

在 controllers 文件夹内,创建一个名为 user.js 的新文件,并添加以下代码:

  // import modules 
  var async = require('async'); 
  var crypto = require('crypto'); 
  var passport = require('passport'); 
  var User = require('../models/User'); 

  // authorization middleware 
  exports.ensureAuthenticated = function(req, res, next) { 
    if (req.isAuthenticated()) { 
      next(); 
    } else { 
      res.redirect('/login'); 
    } 
  }; 

  // logout 
  exports.logout = function(req, res) { 
    req.logout(); 
    res.redirect('/'); 
  };

添加登录的 GET 和 POST 方法

在 controllers/user.js 文件中添加以下代码,就在之前的代码后面:

    // login GET 
    exports.loginGet = function(req, res) { 
      if (req.user) { 
        return res.redirect('/'); 
      } 
      res.render('login', { 
        title: 'Log in' 
      }); 
    }; 

    // login POST 
    exports.loginPost = function(req, res, next) { 
      // validate login form fields 
      req.assert('email', 'Email is not valid').isEmail(); 
      req.assert('email', 'Empty email not allowed').notEmpty(); 
      req.assert('password', 'Empty password not allowed').notEmpty(); 
      req.sanitize('email').normalizeEmail({ remove_dots: false }); 

      var errors = req.validationErrors(); 
      if (errors) { 
        // Show errors messages for form validation 
        req.flash('error', errors); 
        return res.redirect('/login'); 
      } 

      passport.authenticate('local', function(err, user, info) { 
        if (!user) { 
          req.flash('error', info); 
          return res.redirect('/login') 
        } 
        req.logIn(user, function(err) { 
          res.redirect('/'); 
        }); 
      })(req, res, next); 
    }; 

添加注册的 GET 和 POST 方法

在 controllers/user.js 文件中添加以下代码,就在之前的代码后面:

    // signup GET 
    exports.signupGet = function(req, res) { 
      if (req.user) { 
        return res.redirect('/'); 
      } 
      res.render('signup', { 
        title: 'Sign up' 
      }); 
    }; 

    // signup POST 
    exports.signupPost = function(req, res, next) { 
      // validate sign up form fields 
      req.assert('name', 'Empty name not allowed').notEmpty(); 
      req.assert('email', 'Email is not valid').isEmail(); 
      req.assert('email', 'Empty email is not allowed').notEmpty(); 
      req.assert('password', 'Password must be at least 4 characters 
        long').len(4); 
      req.sanitize('email').normalizeEmail({ remove_dots: false }); 

      var errors = req.validationErrors(); 

      if (errors) { 
        // Show errors messages for form validation 
        req.flash('error', errors); 
        return res.redirect('/signup'); 
      } 

      // Verify user email 
      User.findOne({ email: req.body.email }, function(err, user) { 
        if (user) { 
          // if used, show message and redirect 
          req.flash('error', { msg: 'The email is already taken.' }); 
          return res.redirect('/signup'); 
        } 
        // create an instance of user model with form data 
        user = new User({ 
          name: req.body.name, 
          email: req.body.email, 
          password: req.body.password 
        }); 
        // save user 
        user.save(function(err) { 
          req.logIn(user, function(err) { 
            res.redirect('/'); 
          }); 
        }); 
      }); 
    }; 

添加账户的 GET 和 UPDATE 方法

在 controllers/user.js 文件中添加以下代码,就在之前的代码后面:

    // profile account page 
    exports.accountGet = function(req, res) { 
      res.render('profile', { 
        title: 'My Account' 
      }); 
    }; 

    // update profile and change password 
    exports.accountPut = function(req, res, next) { 
      // validate sign up form fields 
      if ('password' in req.body) { 
        req.assert('password', 'Password must be at least 4 characters
        long').len(4); 
        req.assert('confirm', 'Passwords must match') 
         .equals(req.body.password); 
      }
      else { 
        req.assert('email', 'Email is not valid').isEmail(); 
        req.assert('email', 'Empty email is not allowed').notEmpty(); 
        req.sanitize('email').normalizeEmail({ remove_dots: false }); 
      } 
      var errors = req.validationErrors(); 

      if (errors) { 
        // Show errors messages for form validation 
        req.flash('error', errors); 
        return res.redirect('/pages'); 
      } 

      User.findById(req.user.id, function(err, user) { 
        // if form field password change 
        if ('password' in req.body) { 
          user.password = req.body.password; 
        }
        else { 
          user.email = req.body.email; 
          user.name = req.body.name; 
        } 
        // save user data 
        user.save(function(err) { 
          // if password field change 
          if ('password' in req.body) { 
            req.flash('success', { msg: 'Password changed.' }); 
          } else if (err && err.code === 11000) { 
            req.flash('error', { msg: 'The email is already taken.' }); 
          } else { 
            req.flash('success', { msg: 'Profile updated.' }); 
          } 
          res.redirect('/account'); 
        }); 
      }); 
    };

添加账户的 DELETE 方法

在 controllers/user.js 文件中添加以下代码,就在之前的代码后面:

    // profile DELETE 
    exports.accountDelete = function(req, res, next) { 
      User.remove({ _id: req.user.id }, function(err) { 
        req.logout(); 
        req.flash('info', { msg: 'Account deleted.' }); 
        res.redirect('/'); 
       }); 
    };

现在我们已经完成了应用程序控制器。

创建模型文件夹和文件

  1. 在根项目文件夹内,创建一个名为 models 的文件夹。

  2. 创建一个名为 User.js 的新文件,并添加以下代码:

      // import modules 
      var crypto = require('crypto'); 
      var bcrypt = require('bcrypt-nodejs'); 
      var mongoose = require('mongoose'); 

      // using virtual attributes 
      var schemaOptions = { 
        timestamps: true, 
        toJSON: { 
          virtuals: true 
        } 
      }; 

      // create User schema 
      var userSchema = new mongoose.Schema({ 
        name: String, 
        email: { type: String, unique: true}, 
        password: String, 
        picture: String 
      }, schemaOptions); 

      // encrypt password 
      userSchema.pre('save', function(next) { 
        var user = this; 
        if (!user.isModified('password')) { return next(); } 
        bcrypt.genSalt(10, function(err, salt) { 
          bcrypt.hash(user.password, salt, null, function(err, hash) { 
            user.password = hash; 
            next(); 
          }); 
        }); 
      }); 
      // Checking equal password 
      userSchema.methods.comparePassword = function(password, cb) { 
        bcrypt.compare(password, this.password, function(err, isMatch) { 
          cb(err, isMatch); 
        }); 
      }; 
      // using virtual attributes 
      userSchema.virtual('gravatar').get(function() { 
        if (!this.get('email')) { 
          return 'https://gravatar.com/avatar/?s=200&d=retro'; 
        } 
        var md5 =
        crypto.createHash('md5').update(this.get('email')).digest('hex'); 
        return 'https://gravatar.com/avatar/' + md5 + '?s=200&d=retro'; 
      }); 

      var User = mongoose.model('User', userSchema); 

      module.exports = User;

创建 public 文件夹和文件

在这个例子中,我们使用了 Bootstrap 框架的 SASS 版本,就像我们在上一章中所做的那样。但是这一次,我们将把源文件存储在不同的位置,即 public/css 文件夹内。让我们创建文件夹和文件:

  1. 在根项目内,创建一个名为 public 的文件夹。

  2. 在 public 文件夹内,创建一个名为 css 的文件夹,在 css 文件夹内创建一个名为 vendor 的文件夹,在 vendor 文件夹内创建一个名为 bootstrap 的文件夹。

  3. 转到github.com/twbs/bootstrap-sass/tree/master/assets/stylesheets/bootstrap,复制所有内容,并粘贴到 public/css/vendor/bootstrap 文件夹内。

  4. 在 public/css/vendor 文件夹内,创建一个名为 _bootstrap.scss 的新文件,并添加以下代码:

      /*! 
      * Bootstrap v3.3.6 (http://getbootstrap.com) 
      * Copyright 2011-2015 Twitter, Inc. 
      * Licensed under MIT (https://github.com/twbs/bootstrap/blob/
        master/LICENSE) 
      */ 

      // Core variables and mixins 
      @import "bootstrap/variables"; 
      @import "bootstrap/mixins"; 

      // Reset and dependencies 
      @import "bootstrap/normalize"; 
      @import "bootstrap/print"; 
      @import "bootstrap/glyphicons"; 

      // Core CSS 
      @import "bootstrap/scaffolding"; 
      @import "bootstrap/type"; 
      @import "bootstrap/code"; 
      @import "bootstrap/grid"; 
      @import "bootstrap/tables"; 
      @import "bootstrap/forms"; 
      @import "bootstrap/buttons"; 

      // Components 
      @import "bootstrap/component-animations";
      @import "bootstrap/dropdowns"; 
      @import "bootstrap/button-groups"; 
      @import "bootstrap/input-groups"; 
      @import "bootstrap/navs"; 
      @import "bootstrap/navbar"; 
      @import "bootstrap/breadcrumbs"; 
      @import "bootstrap/pagination"; 
      @import "bootstrap/pager"; 
      @import "bootstrap/labels"; 
      @import "bootstrap/badges"; 
      @import "bootstrap/jumbotron"; 
      @import "bootstrap/thumbnails"; 
      @import "bootstrap/alerts"; 
      @import "bootstrap/progress-bars"; 
      @import "bootstrap/media"; 
      @import "bootstrap/list-group"; 
      @import "bootstrap/panels"; 
      @import "bootstrap/responsive-embed"; 
      @import "bootstrap/wells"; 
      @import "bootstrap/close"; 

      // Components w/ JavaScript 
      @import "bootstrap/modals"; 
      @import "bootstrap/tooltip"; 
      @import "bootstrap/popovers"; 
      @import "bootstrap/carousel"; 

      // Utility classes 
      @import "bootstrap/utilities"; 
      @import "bootstrap/responsive-utilities";

创建自定义样式表

在 public/css/文件夹内,创建一个名为 main.scss 的新文件,并添加以下代码:

    // import bootstrap 
    @import "vendor/bootstrap"; 

    // Structure 
    html { 
      position: relative; 
      min-height: 100%; 
    } 

    body { 
      margin-bottom: 44px; 
    } 

    footer { 
      position: absolute; 
      width: 100%; 
      height: 44px; 
      padding: 10px 30px; 
      bottom: 0; 
      background-color: #fff; 
      border-top: 1px solid #e0e0e0; 
    } 

    .login-container { 
      max-width: 555px; 
    } 

    // Warning 
    .alert { 
      border-width: 0 0 0 3px; 
    } 

    // Panels 
    .panel { 
      border: solid 1px rgba(160, 160, 160, 0.3); 
      box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); 
    } 

    .panel-heading + .panel-body { 
      padding-top: 0; 
    } 

    .panel-body { 
      h1, h2, h3, h4, h5, h6 { 
        margin-top: 0; 
      } 
    } 

    .panel-title { 
      font-size: 18px; 
      color: #424242; 
    } 

    // Form 
    textarea { 
      resize: none; 
    } 

    .form-control { 
      height: auto; 
      padding: 8px 12px; 
      border: 2px solid #ebebeb; 
      border-radius: 0; 
      box-shadow: inset 0 1px 2px rgba(150, 160, 175, 0.1), inset 0 1px
      15px rgba(150, 160, 175, 0.05); 
    } 

    .form-group > label { 
      text-transform: uppercase; 
      font-size: 13px; 
    } 

现在不用担心 node-sass 的构建过程;我们已经在本章开头的 package.json 文件中设置了一个 NPM 任务。

创建字体文件夹并添加字体文件

由于我们正在使用 Bootstrap 框架,我们需要一个文件夹来存放所有的 Bootstrap 字体文件,让我们创建一个:

  1. 在 public 文件夹内,创建一个名为 fonts 的新文件夹。

  2. 转到github.com/twbs/bootstrap-sass/tree/master/assets/fonts/bootstrap,复制所有内容,并粘贴到 public/fonts 文件夹内。

创建 JavaScript 文件夹和文件

由于我们正在使用 Bootstrap 框架,我们需要一个文件夹来存放所有的 Bootstrap 字体文件,让我们创建一个:

  1. 在 public 文件夹内,创建一个名为 js 的新文件夹。

  2. 在 js 文件夹内,创建一个名为 lib 的新文件夹。

  3. 在 js/lib 内创建一个名为 bootstrap.js 的新文件。

  4. 转到github.com/twbs/bootstrap-sass/blob/master/assets/javascripts/bootstrap.js,复制所有内容,并粘贴到 public/js/lib/bootstrap.js 文件中。

  5. 在 js/lib 内,创建一个名为 jquery.js 的新文件。

  6. 转到cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js,复制所有内容,并粘贴到 public/js/lib/jquery.js 文件中。

创建视图文件夹和文件

现在我们将创建一个与第一章中非常相似的文件夹结构,使用 MVC 设计模式构建类似 Twitter 的应用程序;views 文件夹将具有以下目录:

/ layouts
/ pages
/ partials

添加 layouts 文件夹和文件

在 views/layouts 文件夹内,创建一个名为 main.html 的新文件,并添加以下代码:

    <!DOCTYPE html> 
    <html> 
      <head> 
        <meta charset="utf-8" /> 
        <meta http-equiv="x-ua-compatible" content="ie=edge"> 
        <meta name="viewport" content="width=device-width, initial-
         scale=1">
        <title>Chapter-10</title> 
          <title>{{title}}</title> 
            <link rel="stylesheet" href="/css/main.css"> 
      </head> 
      <body> 
        {% include "../partials/header.html" %} 
          {% block content %} 
          {% endblock %} 
        {% include "../partials/footer.html" %} 
          <script src="img/jquery.js"></script> 
          <script src="img/bootstrap.js"></script> 
          <script src="img/main.js"></script> 
      </body> 
    </html>

添加 pages 文件夹和文件

现在是时候创建应用程序模板文件了:

  1. 在 views/pages 文件夹内,创建一个名为 home.html 的文件,并添加以下代码:
      {% extends '../layouts/main.html' %} 

      {% block content %} 
        <div class="container"> 
          {% if messages.success %} 
            <div role="alert" class="alert alert-success"> 
              {% for item in messages.success %} 
                <div>{{ item.msg }}</div> 
              {% endfor %} 
            </div> 
          {% endif %} 
          {% if messages.error %} 
            <div role="alert" class="alert alert-danger"> 
              {% for item in messages.error %} 
                <div>{{ item.msg }}</div> 
              {% endfor %} 
            </div> 
          {% endif %} 
          {% if messages.info %} 
            <div role="alert" class="alert alert-info"> 
              {% for item in messages.info %} 
                <div>{{ item.msg }}</div> 
              {% endfor %} 
            </div> 
          {% endif %} 
          <div class="app"> 
            <div class="jumbotron"> 
              <h1 class="text-center">Node 6 Bluenprints Book</h1> 
                <p>This example illustrate how to build, test and deploy
                   a Node.js application using: Github, Heroku, MOngolab,
                   Codeship and Docker.
                </p> 
            </div> 
          </div> 
        </div> 
      {% endblock %}

  1. 在 views/pages 文件夹内,创建一个名为 login.html 的文件,并添加以下代码:
      {% extends '../layouts/main.html' %} 

      {% block content %} 
        <div class="login-container container"> 
          <div class="panel"> 
            <div class="panel-body"> 
              {% if messages.error %} 
                <div role="alert" class="alert alert-danger"> 
                  {% for item in messages.error %} 
                    <div>{{ item.msg }}</div> 
                  {% endfor %} 
                </div> 
              {% endif %} 
              <form method="POST"> 
                <legend>Welcome to login</legend> 
                <div class="form-group"> 
                  <label for="email">Email</label> 
                  <input type="email" name="email" id="email" 
                    placeholder="Email" class="form-control" autofocus> 
                </div> 
                <div class="form-group"> 
                  <label for="password">Password</label> 
                  <input type="password" name="password" id="password"
                   placeholder="Password" class="form-control"> 
                </div> 
                <button type="submit" class="btn btn-primary btn-block">
                  Sign in</button> 
              </form> 
            </div> 
          </div> 
          <p class="text-center">Don't have an account? <a href="/signup">
            <strong>Sign up</strong></a>, it's free.</p> 
        </div> 
      {% endblock %}

  1. 在 views/pages 文件夹内,创建一个名为 profile.html 的文件,并添加以下代码:
      {% extends '../layouts/main.html' %} 

      {% block content %} 
        <div class="container"> 
          <div class="panel"> 
            <div class="panel-body"> 
              {% if messages.success %} 
                <div role="alert" class="alert alert-success"> 
                  {% for item in messages.success %} 
                    <div>{{ item.msg }}</div> 
                  {% endfor %} 
                </div> 
              {% endif %} 
              {% if messages.error %} 
                <div role="alert" class="alert alert-danger"> 
                  {% for item in messages.error %} 
                    <div>{{ item.msg }}</div> 
                  {% endfor %} 
                </div> 
              {% endif %} 
              <form method="POST" action="/account?_method=PUT"> 
                <legend>Account Details</legend> 
                <div class="form-group"> 
                    <label for="email">Email</label> 
                    <input type="email" name="email" id="email"
                      class="form-control" value="{{user.email}}"> 
                </div> 
                <div class="form-group"> 
                    <label for="name">Name</label> 
                    <input type="text" name="name" id="name" class="form-
                      control" value="{{user.name}}"> 
                </div> 
                  <br> 
                <div class="form-group"> 
                    <button type="submit" class="btn btn-primary">
                      Update Profile</button> 
                </div> 
              </form> 
            </div> 
          </div> 
          <div class="panel"> 
            <div class="panel-body"> 
              <form method="POST" action="/account?_method=PUT"> 
                <legend>Change Password</legend> 
                <div class="form-group"> 
                  <label for="password">New Password</label> 
                    <input type="password" name="password" id="password"
                      class="form-control"> 
                </div> 
                <div class="form-group"> 
                  <label for="confirm">Confirm Password</label> 
                    <input type="password" name="confirm" id="confirm"
                      class="form-control"> 
                </div> 
                <div class="form-group"> 
                    <button type="submit" class="btn btn-success">
                      Change Password</button> 
                </div> 
              </form> 
            </div> 
          </div> 
          <div class="panel"> 
            <div class="panel-body"> 
              <form method="POST" action="/account?_method=DELETE"> 
                <legend>Delete My Account</legend> 
                <div class="form-group"> 
                  <p class="text-muted">It is irreversible action.</p> 
                    <button type="submit" class="btn btn-danger">
                      Delete</button> 
                  </div> 
              </form> 
            </div> 
          </div> 
        </div> 
      {% endblock %}
  1. 在 views/pages 文件夹内,创建一个名为 signup.html 的文件,并添加以下代码:
      {% extends '../layouts/main.html' %} 

      {% block content %} 
      <div class="login-container container"> 
        <div class="panel"> 
          <div class="panel-body"> 
            {% if messages.error %} 
              <div role="alert" class="alert alert-danger"> 
                {% for item in messages.error %} 
                  <div>{{ item.msg }}</div> 
                {% endfor %} 
              </div> 
            {% endif %} 
            <form method="POST"> 
              <legend>Create an account</legend> 
              <div class="form-group"> 
                <label for="name">Name</label> 
                <input type="text" name="name" id="name" 
                  placeholder="Name"
                  class="form-control" autofocus> 
              </div> 
              <div class="form-group"> 
                <label for="email">Email</label> 
                <input type="email" name="email" id="email"
                  placeholder="Email" class="form-control"> 
              </div> 
              <div class="form-group"> 
                <label for="password">Password</label> 
                <input type="password" name="password" id="password" 
                  placeholder="Password" class="form-control"> 
              </div> 
              <button type="submit" class="btn btn-primary btn-block">
                Sign up</button> 
            </form> 
          </div> 
        </div> 
        <p class="text-center"> Already have an account? <a href="/login">
          <strong>Sign in</strong></a></p> 
      </div> 
      {% endblock %}

添加 partial 文件夹和文件

与上一章一样,我们使用了 partial views 的概念,因此让我们创建 views 文件:

  1. 在 views/partials 文件夹内,创建一个名为 footer.html 的新文件,并添加以下代码:
      <footer> 
        <div class="container"> 
          <p>Node.js 6 Blueprints Book © 2016\. All Rights Reserved.</p> 
        </div> 
      </footer>

  1. 在 views/partials 文件夹内,创建一个名为 header.html 的新文件,并添加以下代码:
      <nav class="navbar navbar-default navbar-static-top"> 
        <div class="container"> 
          <div class="navbar-header"> 
            <button type="button" data-toggle="collapse"
              data-target="#navbar" class="navbar-toggle collapsed"> 
              <span class="sr-only">Toggle navigation</span> 
              <span class="icon-bar"></span> 
              <span class="icon-bar"></span> 
              <span class="icon-bar"></span> 
            </button> 
            <a href="/" class="navbar-brand">N6B</a> 
          </div> 
          <div id="navbar" class="navbar-collapse collapse"> 
            <ul class="nav navbar-nav"> 
              <li class="{% if  title == 'Home' %}active{% endif %}">
              <a href="/">Home</a></li> 
            </ul> 
          <ul class="nav navbar-nav navbar-right"> 
          {% if user %} 
            <li class="dropdown"> 
              <a href="#" data-toggle="dropdown"
                class="navbar-avatar dropdown-toggle"> 
                {% if user.picture %} 
                  <img src="img/{{user.picture}}"> 
                {% else %} 
                  <img src="img/{{user.gravatar}}"> 
                {% endif %} 
                {% if user.name %} 
                  {{user.name}} 
                {% else %} 
                  {{user.email}} 
                {% endif %} 
                <i class="caret"></i> 
              </a> 
              <ul class="dropdown-menu"> 
                <li><a href="/account">My Account</a></li> 
                <li class="divider"></li> 
                <li><a href="/logout">Logout</a></li> 
              </ul> 
            </li> 
          {% else %} 
            <li class="{% if  title == 'Log in' %}active{% endif %}">
              <a href="/login">Log in</a></li> 
            <li class="{% if  title == 'Sign up' %}active{% endif %}">
              <a href="/signup">Sign up</a></li> 
          {% endif %} 
          </ul> 
          </div> 
        </div> 
      </nav>

到目前为止,我们的应用程序几乎已经准备好部署了,但在继续之前,我们需要创建测试文件夹和文件。

创建测试文件夹和测试文件

要在 Node.js 应用程序上运行测试,我们需要包含一些依赖/模块来帮助我们编写这些测试。幸运的是,在 Node 生态系统中,我们有许多模块可供使用。

接下来,我们将描述如何使用Supertest模块和Mocha测试运行器为 HTTP 服务器编写测试。我们将需要的模块插入到我们的 package.json 文件中:

"devDependencies": { 
    "mocha": "².4.5", 
    "supertest": "¹.2.0" 
  } 

模块 描述 更多信息
Mocha 测试框架 mochajs.org
Supertest 用于测试 HTTP 服务器 www.npmjs.com/package/supertest

Web 应用程序的测试是一个非常复杂的主题,值得深入研究,但我们将看到如何编写单元测试以及如何在 Node.js 应用程序中使用 Mocha 运行测试:

  1. 在根项目文件夹内,创建一个名为 test 的新文件夹。

  2. 在 test/文件夹内,创建一个名为 app-test.js 的新文件,并添加以下代码:

      // import modules 
      var request = require('supertest'); 
      var server = require('../server'); 

      // Test 01 
      describe('GET /', function() { 
        it('should render ok', function(done) { 
          request(server) 
          .get('/') 
          // expected result 
          .expect(200, done); 
        }); 
      }); 

      // Test 02 
      describe('GET /bikes', function() { 
        it('should not found', function(done) { 
          request(server) 
          .get('/bikes') 
          // expected result 
         .expect(404, done); 
       }); 
     }); 

测试用例非常简单:

  • 测试 01:

检查根 URL,并期望获得 HTTP 状态码 200。

  • 测试 02:

期望获得 HTTP 状态码 404。

现在让我们看看如何执行这些测试。

提示

请注意,您必须在计算机上运行 MongoDB。

  1. 用以下信息替换根项目文件夹中的.env 文件:
      SESSION_SECRET="3454321234" 
      MONGODB="localhost" 

请注意,稍后在本书中,我们将把此文件恢复为其原始状态。

  1. 在根项目文件夹中打开终端/ shell,并输入以下命令:
npm install

  1. 现在输入以下命令:
npm test

您应该在终端输出中看到以下结果:

创建测试文件夹和测试文件

测试后的终端输出

请注意,两个测试都通过了,左侧的测试描述旁边有一个绿色的勾号图标。

运行应用程序

现在是时候检查应用程序了:

  1. 打开终端并输入以下命令:
 npm start

  1. 转到 http://localhost:3000/signup 并使用以下信息创建一个新帐户:

姓名:John Doe

电子邮件:john@doe.com

密码:123456

运行应用程序

注册页面

注册流程后,转到 http://localhost:3000/account 并查看以下屏幕截图,其中包含用户信息:

运行应用程序

帐户信息

创建一个 GitHub 或 Bitbucket 免费帐户

您可以选择使用哪种服务,因为 GitHub 和 Bitbucket 都可以做同样的事情:托管公共和私有代码存储库,用于协作软件开发。

两者的功能都类似,都使用 git 作为源代码控制。我们将看到如何使用 GitHub,但 Bitbucket 的过程非常相似。

提示

您可以在此链接找到有关 Bitbucket 的更多信息:bitbucket.org/

创建 GitHub 免费账户

让我们创建一个 GitHub 账户:

  1. 前往github.com/join,填写表格,然后点击创建账户按钮。

  2. 选择免费无限公共存储库复选框,然后点击继续按钮。

  3. 在第三步,您必须回答三个问题或选择跳过此步骤;点击提交按钮。从这里,您可以阅读指南或开始一个项目。

提示

请注意,您需要在开始项目之前验证您的电子邮件。

  1. 点击开始项目按钮,并填写存储库名称,记住您可以在 GitHub 上使用这个名称,但您需要为 Heroku 和 Codeship 过程选择另一个名称。之后,您将看到以下截图:创建 GitHub 免费账户

GitHub 项目

在本章的后面,我们将看到如何初始化本地 GIT 存储库以及如何将源代码推送到 GitHub 存储库。

创建 Heroku 免费账户

在上一章中,我们使用 Heroku 工具包命令直接将应用程序部署到 Heroku。这一次,您可以使用我们在第九章中创建的相同账户,使用 Node.js 和 NPM 构建前端流程,或者在www.heroku.com/创建一个新账户。

创建 MongoLab 免费沙盒账户

MongoLab 是使用 MongoDB 作为服务所需的云服务。它提供了一个免费的有限账户作为沙盒,所以我们可以用它来部署我们的项目:

  1. 继续到注册页面;之后,您将收到来自 MongoLab 的两封电子邮件,一封是欢迎消息,另一封是验证您的账户的链接,如果您还没有账户。

验证您的账户后,登录到仪表板时,您将看到以下截图:

创建 MongoLab 免费沙盒账户

MongoLab 欢迎界面

  1. 点击创建新按钮。

  2. 选择单节点选项卡。

  3. 从标准行面板中,选择沙盒的第一个复选框。

  4. 向下滚动到页面底部,插入数据库名称nb6,然后点击创建新的 mongodb 部署按钮。

在这五个步骤结束时,您应该看到以下屏幕:

创建 MongoLab 免费沙盒账户

在 MongoLab 创建的数据库

为数据库创建用户名和密码

现在是时候创建用户名和密码来保护我们在云上的数据库了:

  1. 点击数据库名称。

您将看到以下警告,建议您创建用户名和密码:

为数据库创建用户名和密码

数据库警告:没有用户和密码

  1. 点击警告消息内的点击这里链接。

  2. 插入以下信息:

      database username: nb6 
      database password: node6 

  1. 点击创建用户按钮。

获取连接字符串

现在我们在 MongoLab 云服务上运行了一个 MongoDB 实例。这是我们将在本章后面使用的连接字符串:

    mongodb://<user>:<password>@ds023074.mlab.com:23074/nb6 

提示

您必须用自己的用户名和密码替换以前的代码。

初始化 git 存储库并推送到 GitHub

此时,我们将创建本地 git 存储库,然后将其上传到我们刚在 GitHub 上创建的账户:

  1. 在根应用程序文件夹中打开终端/Shell,然后输入以下命令:
 git init

  1. 通过在终端/Shell 中输入以下命令,将远程存储库添加到项目中:
 git remote add origin https://github.com/<your github account 
       name>/n6b.git

您必须在以前的代码中使用自己的 github 用户名。

  1. 通过在终端/Shell 中键入以下命令将所有项目文件添加到源代码控制:
 git add .

  1. 通过在终端/Shell 中键入以下命令提交项目更改:
 git commit -m "initial commit"

最后一个命令是将所有文件上传到我们之前创建的 GitHub 存储库。

  1. 在终端/Shell 中键入以下命令:
 git push -u origin master

使用 Heroku 仪表板创建 Heroku 应用程序

这次,我们将看到另一种使用 Heroku 云服务创建项目的方法:

  1. 转到dashboard.heroku.com/apps

  2. 在 Heroku 仪表板上,单击新建按钮,然后单击创建新应用链接。

  3. 在应用程序输入名称字段中输入以下名称:chapter-10-yourname

  4. 点击创建应用按钮。

将 Heroku 应用程序链接到您的 git 存储库

现在我们需要设置我们的 Heroku 帐户以链接到我们的 github 帐户。所以让我们按照以下步骤进行:

  1. 在 Heroku 仪表板上,点击 chapter-10-yourname 项目名称。

  2. 点击设置选项卡,向下滚动页面到,并复制 Heroku 域 URL:

chapter-10-yourname.herokuapp.com

提示

请注意,我们不能为所有应用程序使用相同的名称,因此您需要在 chapter-10 之后提供您的名称。

稍后我们将使用应用程序名称来配置 Codeship 部署管道,所以不要忘记它。

向 Heroku 添加环境变量

现在我们需要创建一些环境变量,以便在我们的公共 github 存储库中保护我们的数据库字符串安全:

  1. 在 Heroku 仪表板上,点击 chapter-10-yourname 项目名称。

  2. 点击设置选项卡。

  3. 设置选项卡中,单击显示配置变量按钮。

  4. 添加您自己的变量,如以下屏幕截图所示。在左侧添加变量名称,在右侧添加值:向 Heroku 添加环境变量

Heroku 环境变量

提示

请注意,您必须在 Codeship 配置项目上重复此过程。

创建 Codeship 免费帐户

Codeship 是一个用于持续集成(CI)工具的云服务。创建帐户非常简单:

  1. 转到codeship.com/sessions/new,并使用右上角的注册按钮。您可以使用 GitHub 或 Bitbucket 帐户;只需点击您偏好的按钮。由于我们使用 GitHub,我们将选择 GitHub。

  2. 单击授权应用程序按钮。

您应该会看到以下屏幕:

创建 Codeship 免费帐户

Codeship 仪表板

下一步是单击您托管代码的位置。在这种情况下,我们将单击GitHub图标,因此我们将看到以下屏幕:

创建 Codeship 免费帐户

Codeship 配置的第二步

  1. 复制并粘贴 GitHub 存储库 URL(https://github.com//n6b.git),该 URL 是在 GitHub 设置过程中创建的,并将其粘贴到存储库克隆 URL输入中,如前图所示。

  2. 点击连接按钮。

现在我们已经使用三种工具(GitHub、Codeship 和 Heroku)设置了开发环境。下一步是创建设置和测试命令,并将管道部署添加到 Codeship 仪表板。

向 Codeship 添加环境变量

现在让我们像我们在 Heroku 仪表板中所做的那样,向 Codeship 添加相同的变量:

  1. 转到codeship.com/projects/,并选择 chapter-10-yourname 项目。

  2. 点击右上角的项目设置链接,如图所示:向 Codeship 添加环境变量

Codeship 仪表板中的项目设置菜单

  1. 点击环境变量链接。

  2. 添加会话和 MongoDB 变量和值,就像我们之前对 Heroku 环境变量的配置所做的那样,并点击保存配置按钮。

在 Codeship 项目配置中创建设置和测试命令

现在我们回到 codeship 控制面板,并为我们的应用程序配置测试和部署命令:

  1. 将以下代码粘贴到设置命令文本区域:
      # By default we use the Node.js version set in your package.json
        or the latest 
      # version from the 0.10 release 
      # 
      # You can use nvm to install any Node.js (or io.js) version you require. 
      # nvm install 4.0 
      # nvm install 0.10 
      npm install 
      npm run build 

  1. 将以下代码粘贴到测试命令文本区域:
 npm test

  1. 点击保存并转到仪表板按钮。

在 Heroku 上创建部署管道

好了,我们几乎到了;现在我们需要创建一个管道,将构建与我们在 Heroku 上的部署环境集成起来:

  1. 点击右上角的项目设置链接,然后点击部署链接,如图所示:在 Heroku 上部署的管道创建

  2. 输入分支名称输入框中,输入以下名称:master。

  3. 点击保存 Pipeline 设置按钮。

  4. 添加新的部署管道选项卡中,选择Heroku横幅。

现在我们将按照这里显示的输入字段填写:

在 Heroku 上部署的管道创建

Codeship Heroku 部署配置面板

将 Heroku API 密钥添加到 Codeship

为了提供前面截图中所需的信息,我们需要按照以下步骤进行:

  1. 打开一个新的浏览器窗口,转到 Heroku 仪表板id.heroku.com/login

  2. 点击右上角的图片,然后点击账户设置

  3. 向下滚动页面以获取 API 密钥,并点击显示 API 密钥,如图所示:将 Heroku API 密钥添加到 Codeship

显示 API 密钥 Heroku 仪表板

  1. 输入密码并复制 API 密钥。

  2. 返回 Codeship 浏览器窗口,并将密钥粘贴到Heroku API 密钥输入字段中。

  3. 命名您的应用程序:n6b-your-own-name。

  4. 添加应用程序 URL:http://chapter-10-your-own-name.herokuapp.com/。

  5. 点击保存部署按钮。

这一步完成了我们的持续集成。每当我们修改代码并将更改发送到 GitHub 或 Bitbucket 时,Codeship 将运行我们在本章前面创建 Node.js 应用程序时设置的代码和测试。

在测试结束时,如果一切正常,我们的代码将被发送到 Heroku,并且将在 http://chapter-10-yourname.herokuapp.com/上可用。

在 Codeship 仪表板上检查测试和部署步骤

在这一点上,我们已经有了设置测试和部署所需的命令,但是我们需要检查一切是否正确配置:

  1. 转到codeship.com/sessions/new

  2. 登录到您的账户。

  3. 在左上角,点击选择项目链接。

  4. 点击 n6b-your-own-name 项目名称,您将看到所有带有成功运行失败标志的提交。

当我们点击其中一个时,我们可以看到以下带有逐步过程的截图:

在 Codeship 仪表板上检查测试和部署步骤

Codeship 构建步骤

在这里我们有一个成功的构建,因为我们可以看到每个步骤右侧的绿色勾号图标。

请注意,如果测试过程在任何时候失败,代码将不会发送到生产服务器,即 Heroku。

安装 Docker 并设置应用程序

在我们继续之前,我们需要了解 Docker 是什么以及容器的概念。

提示

您可以在此链接阅读有关容器的更多信息:www.docker.com/what-docker#/VM

简单来说,Docker 创建微型机器(即操作系统)在一个隔离的盒子内运行您的应用程序,无论您的平台是 Windows、Linux 还是 OSX。让我们看看官方 Docker 网站上说了什么:

"Docker 容器将软件包装在一个完整的文件系统中,其中包含运行所需的一切:代码、运行时、系统工具、系统库 - 任何可以安装在服务器上的东西。"

所以让我们在你的机器上安装 Docker。Windows 机器需要特别注意,但你可以在以下链接找到相关信息:docs.docker.com/engine/installation/windows/。转到docs.docker.com/并按照你的平台的说明进行操作。

检查 Docker 版本

现在是时候检查你的机器上安装的 Docker 版本了:

打开终端/Shell 并输入以下命令来检查每个部分的版本:

 docker --version
 docker-compose --version
 docker-machine --version

创建一个 Docker 文件

为了将我们的应用程序 docker 化到一个容器中,我们需要创建两个文件,一个 Dockerfile 和一个 docker-compose.yml 文件,以将我们的应用程序容器与 MongoDb 数据库链接起来:

  1. 在根文件夹内,创建一个名为 Dockerfile 的新文件,并添加以下代码:
      FROM node:argon 

      # Create app directory 
      RUN mkdir -p /usr/src/app 
      WORKDIR /usr/src/app 

      # Install app dependencies 
      COPY package.json /usr/src/app/ 
      RUN npm install 

      ENV PORT 3000 
      ENV DB_PORT_27017_TCP_ADDR db 

      # Bundle app source 
      COPY . /usr/src/app 

      EXPOSE 3000 
      CMD [ "npm", "start" ] 

请注意,行 ENV DB_PORT_27017_TCP_ADDR 指示了 MongoDB 的 Docker 容器端口;这是一个环境变量。

  1. 在根文件夹内,创建一个名为 docker-compose.yml 的新文件,并添加以下代码:
      app: 
        build: . 
        ports: 
          - "3000:3000" 
        links: 
          - db 

      db: 
        image: mongo 
        ports: 
          - "27017:27017" 

db 行已设置为 ENV DB_PORT_27017_TCP_ADDR db 名称。

在我们继续之前,让我们检查一些有用的 Docker 命令:

命令 描述
docker ps -a 列出所有容器
docker images 列出所有镜像
docker rm containername 删除特定容器
docker rm $(docker ps -a -q) 删除所有容器
docker rmi imagename 删除特定镜像
docker rmi $(docker images -q) 删除所有镜像
docker run containername 运行一个容器
docker stop containername 停止一个容器
docker stop $(docker ps -a -q) 停止所有容器

我们有更多的命令,但在本章的过程中我们会看到其他命令。

创建一个 Docker 镜像

在这一点上,我们已经有了设置测试和部署所需的命令,但是我们需要检查一切是否正确配置:

  1. 为你的项目创建 Docker 镜像:
 docker build -t <your docker user name>/<projectname> .

在终端的输出末尾,我们可以看到类似于这样的消息:成功构建 c3bbc61f92a6。现在让我们检查已经创建的镜像。

  1. 通过打开终端/Shell 并输入以下命令来检查图像:
 docker images

准备和运行 Docker 镜像

现在让我们测试我们的 Docker 镜像。在我们继续之前,我们需要对我们的应用程序进行一些小改动:

  1. 打开根文件夹中的 server.js 文件,并替换以下代码:
      mongoose.connect('mongodb://' + (process.env.DB_PORT_27017_TCP_ADDR
      || process.env.MONGODB) + '/<database name>'); 

  1. 现在打开.env 文件,并用以下行替换代码:
      SESSION_SECRET=
       'ae37a4318f1218302e16e1516e4144df8a273798b151ca06062c142bbfcc23bc' 

      MONGODB='localhost:27017' 

提示

步骤 1 和步骤 2 使用本地凭据,与我们为部署所做的不同。因此,在配置 Heroku 和 Codeship 的环境变量后,从 GitHub 跟踪中删除.env 文件,但在本地机器上保留具有本地凭据的文件。

现在是时候从 Docker hub 获取一个 MongoDB 镜像了。

  1. 打开终端/Shell 并输入以下命令:
 docker pull mongo

上一个命令将获取一个新的 MongoDB 镜像。你可以使用相同的命令从 Docker hub 获取任何镜像。

提示

你可以在这个链接上找到更多的图像:hub.docker.com/explore/

  1. 用以下命令启动一个新的名为 db 的 MongoDB 容器:
 docker run -d --name db mongo

  1. 现在我们需要将一个容器链接到另一个;输入以下命令:
docker run -p 3000:3000 --link db:db <your docker user name>
        /<projectname>

  1. 转到 http://localhost:3000,你将看到你的应用程序正在运行。它看起来和在你的机器上一样。

将项目图像上传到您的 Docker hub 帐户

现在,是时候将你的图像上传到 Docker hub,并让其他用户可以使用了。

提示

你可以在这个链接上阅读更多关于Docker hub的信息:docs.docker.com/docker-hub/

  1. 转到cloud.docker.com/并创建一个免费帐户。

  2. 确认你的电子邮件地址后,转到cloud.docker.com登录菜单。你将看到以下仪表板:将项目图像上传到您的 Docker hub 帐户

Docker hub 仪表板

当您点击存储库按钮时,您会发现它是空的。现在让我们将我们的 Docker 图像推送到 Docker hub。

  1. 打开终端/Shell 并输入以下命令:
 docker login

输入您的凭据,这些是您在注册过程中创建的。

  1. 要将项目上传到 Docker hub,请在终端/Shell 上运行以下命令:
 docker push <your docker user name>/<projectname>

  1. 返回到cloud.docker.com/_/repository/list,刷新页面,您将看到您的存储库已发布在 Docker hub 上。

Docker 是一个强大的工具,必须进一步探索,但从本章开始,我们已经有足够的知识来使用 Docker 容器构建 Node.js 应用程序与 MongoDB,这意味着您可以在任何机器上使用我们创建的容器。无论您使用什么平台,您只需要安装 Docker 并将图像拉到您的机器上。

您可以获取任何图像,并在命令行上开始使用它。

总结

到本章结束时,您应该能够使用我们目前可用的所有最现代化的技术和工具来构建和部署应用程序,从而创建令人惊叹的 Web 应用程序。

我们已经探索了构建应用程序所需的所有资源,使用持续交付和持续集成,结合 Git 源代码控制,GitHub,Codeship,Heroku 和 Docker。我们还看到了如何在 Heroku 云服务的生产环境和 Codeship 的测试和持续集成中使用环境变量。

posted @ 2024-05-23 15:56  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报