NodeJS-6-x-蓝图-全-
NodeJS 6.x 蓝图(全)
原文:
zh.annas-archive.org/md5/9B48011577F790A25E05CA5ABA4F9C8B
译者:飞龙
前言
使用 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-parser
、body-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 的欢迎消息。
更改应用程序的结构
让我们对应用程序的目录结构进行一些更改,并准备好遵循模型-视图-控制器设计模式。
我将列出这次重构的必要步骤:
- 在
root
项目文件夹内:
- 创建一个名为
server
的新文件夹
- 在
server
文件夹内:
-
创建一个名为
config
的新文件夹 -
创建一个名为
routes
的新文件夹 -
创建一个名为
views
的新文件夹。
-
此时不要担心
config
文件夹;我们稍后会插入它的内容。 -
现在我们需要将
chapter-01/views
文件夹中的error.js
和index.js
文件移动到chapter-01/server/views
文件夹中。 -
将
chapter-01/routes
文件夹中的index.js
和user.js
文件移动到chapter-01/server/routes
文件夹中。 -
这里只有一个非常简单的更改,但在开发过程中,更好地组织我们应用程序的所有文件将非常有用。
我们仍然需要在主应用程序文件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' });
});
要运行项目并在浏览器中查看应用程序,请按照以下步骤操作:
- 在你的终端/Shell 中输入以下命令:
DEBUG=chapter-01:* npm start
-
在你的浏览器中打开
http://localhost:3000
。 -
在你的浏览器中的输出将如下所示:
应用程序主屏幕
现在我们可以删除以下文件夹和文件:
-
chapter-01/routes
: -
index.js
-
user.js
-
chapter-01/views
: -
error.js
-
index.js
更改默认行为以启动应用程序
如前所述,我们将更改应用程序的默认初始化过程。为了完成这个任务,我们将编辑app.js
文件并添加几行代码:
- 打开
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
。
- 现在在
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);
});
- 打开项目根目录下的
package.js
文件,并更改以下代码:
...
"scripts": {
"start": "node app.js"
},
...
注意
如果需要,仍然可以使用调试命令:DEBUG=chapter-01:* npm start
。
-
package.json
文件是 Node.js 应用程序中极其重要的文件。它可以存储项目的各种信息,如依赖关系、项目描述、作者、版本等等。 -
此外,还可以设置脚本来轻松地进行缩小、连接、测试、构建和部署应用程序。我们将在第九章中看到如何创建脚本,使用 Node.js 和 NPM 构建前端流程。
-
让我们测试一下结果;打开你的终端/Shell 并输入以下命令:
npm start
- 我们将在控制台上看到相同的输出:
> node app.js
Express server listening on port 3000!
使用部分文件重构视图文件夹
现在我们将对views
文件夹中的目录结构进行重大更改:我们将添加一个重要的嵌入式 JavaScript(EJS)资源,用于在我们的模板中创建可重用的文件。
它们被称为部分文件,并将使用<% = include %>
标签包含在我们的应用程序中。
提示
您可以在官方项目页面ejs.co/
上找到有关EJS的更多信息。
在views
文件夹中,我们将创建两个名为partials
和pages
的文件夹:
-
此时
pages
文件夹将如下所示: -
现在让我们将在
views
文件夹中的文件移动到pages
文件夹中。 -
在
views
文件夹内创建一个pages
文件夹。 -
在
views
文件夹内创建一个partials
文件夹。
-
server/
-
pages/
-
index.ejs
-
error.ejs
-
partials/
-
现在我们需要创建将包含在所有模板中的文件。请注意,我们只有两个模板:
index.js
和error.js
。 -
创建一个名为
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。
-
我们正在使用内容传送网络(CDN)来获取CSS和JS文件。
-
创建一个名为
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>
- 然后创建一个名为
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 -->
- 创建一个名为
footer.ejs
的文件,并添加以下代码:
<footer class="footer">
<div class="container">
<span>© 2016\. Node-Express-MVC-App</span>
</div>
</footer>
- 让我们在
app.js
文件中调整视图模板的路径;添加以下代码:
// view engine setup
app.set('views', path.join(__dirname, 'server/views/pages'));
app.set('view engine', 'ejs');
提示
请注意,我们只添加了已经存在的pages
文件夹路径。
- 现在我们将用以下代码替换
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>
- 让我们对
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
为登录、注册和个人资料添加模板
现在我们有了一个坚实的基础,可以继续进行项目。此时,我们将为登录、注册和个人资料界面添加一些模板文件。
这些页面的预期结果将如下截图所示:
登录界面
注册界面
个人资料界面
- 现在让我们创建登录模板。在
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">×</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>
- 在
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
中间件来显示错误消息。稍后,我们将展示如何安装这个组件;现在不用担心。
-
让我们将
signup
模板添加到views/pages
文件夹中。 -
在
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>
- 现在我们需要为注册视图添加路由。打开
routes/index.js
并在登录路由
之后添加以下代码:
/* GET Signup */
router.get('/signup', function(req, res) {
res.render('signup', { title: 'Signup Page',
message:req.flash('signupMessage') });
});
- 接下来,我们将在
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>
- 现在我们需要为 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 显示用户图标。在本节中,我们将看到如何安装一些非常重要的模块用于我们的应用程序。
由于我们为signin
、signup
和profile
页面创建了模板,我们需要存储具有登录和密码的用户。
这些是我们将用于此任务的中间件,每个中间件的定义如下:
组件 | 描述 | 更多细节 |
---|---|---|
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
文件进行重大重构,以包含我们将使用的新中间件。
我们将逐步向您展示如何包含每个中间件,最后我们将看到完整的文件:
- 打开
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');
这是一个简单的导入过程。
- 在
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);
-
请注意,我们在第一行使用了一个
config.js
文件;稍后我们将创建这个文件。 -
在
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
文件:
- 在
server/config
内创建一个名为config.js
的文件,并将以下代码放入其中:
// Database URL
module.exports = {
// Connect with MongoDB on local machine
'url' : 'mongodb://localhost/mvc-app'
};
- 在
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);
保护路由
到目前为止,我们已经有足够的代码来配置对我们应用程序的安全访问。但是,我们仍然需要添加一些行到登录和注册表单中,以使它们正常工作:
- 打开
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
}));
- 在
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
}));
- 现在让我们添加一个简单的函数来检查用户是否已登录;在
server/routes/index.js
的末尾添加以下代码:
/* check if user is logged in */
function isLoggedIn(req, res, next) {
if (req.isAuthenticated())
return next();
res.redirect('/login');
}
- 让我们添加一个简单的路由来检查用户是否已登录,并在
isLoggedIn()
函数之后添加以下代码:
/* GET Logout Page */
router.get('/logout', function(req, res) {
req.logout();
res.redirect('/');
});
- 最后的更改是将
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
文件夹,这样我们可以分离路由和控制器函数,从而实现更好的模块化:
-
创建一个名为
controllers
的文件夹。 -
创建一个名为
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');
};
- 让我们在
app.js
文件中导入控制器;在var users = require('./server/routes/users')
之后添加以下行:
// Import comments controller
var comments = require('./server/controllers/comments');
- 现在在
app.use('/users', users)
之后添加评论路由:
// Setup routes for comments
app.get('/comments', comments.hasAuthorization, comments.list);
app.post('/comments', comments.hasAuthorization, comments.create);
- 在
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">×</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>
- 请注意,我们使用了 Twitter-bootstrap 的简单 Modal 组件来添加评论,如下截图所示:
创建评论屏幕的模型
- 最后一步是为评论创建一个模型;让我们在
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);
运行应用程序并添加评论
现在是时候测试一切是否正常工作了:
- 在项目根目录打开终端/Shell,并输入以下命令:
npm start
-
在浏览器中检查:
http://localhost:3000
。 -
转到
http://localhost:3000/signup
,创建一个名为John Doe
的用户,邮箱为john@doe.com
,密码为123456
。 -
转到
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.
- 以下截图展示了最终结果:
评论屏幕
检查错误消息
现在让我们检查 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 的应用程序。
-
创建一个名为
chapter-02
的文件夹。 -
在此文件夹中打开您的终端/ shell 并键入 express 命令:
express --git
请注意,这次我们只使用了--git
标志,我们将使用另一个模板引擎,但将手动安装它。
安装 Swig 模板引擎
要做的第一步是将默认的 express 模板引擎更改为Swig,这是一个非常简单、灵活和稳定的模板引擎,还为我们提供了一个非常类似于 AngularJS 的语法,只需使用双大括号{{ variableName }}
表示表达式。
提示
有关Swig的更多信息,请访问官方网站:github.com/paularmstrong/swig
。
- 打开
package.json
文件并用以下代码替换jade
行:
"swig": "¹.4.2",
- 在项目文件夹中打开终端/ shell 并键入:
npm install
- 在我们继续之前,让我们对
app.js
进行一些调整,我们需要添加Swig
模块。打开app.js
并在var bodyParser = require('body-parser');
行之后添加以下代码:
var swig = require('swig');
- 用以下代码替换默认的
jade
模板引擎行:
var swig = new swig.Swig();
app.engine('html', swig.renderFile);
app.set('view engine', 'html');
重构 views 文件夹
与之前一样,让我们将views
文件夹更改为以下新结构:
views
-
pages/
-
partials/
-
从
views
文件夹中删除默认的jade
文件。 -
在
pages
文件夹中创建一个名为layout.html
的文件并放入以下代码:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
- 在
views/pages
文件夹中创建一个index.html
并放入以下代码:
{% extends 'layout.html' %}
{% block title %}{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
Welcome to {{ title }}
{% endblock %}
- 在
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 %}
- 我们需要在
app.js
上调整views
路径,并在var app = express();
函数之后用以下代码替换代码:
// view engine setup
app.set('views', path.join(__dirname, 'views/pages'));
此时,我们已经完成了启动 MVC 应用程序的第一步。在上一章中,我们基本上使用了 express 命令创建的原始结构,但在本例中,我们将完全使用 MVC 模式,即 Model,View,Controller。
创建一个 controllers 文件夹
-
在根项目文件夹内创建一个名为
controllers
的文件夹。 -
在
controllers
文件夹中创建一个index.js
并放入以下代码:
// Index controller
exports.show = function(req, res) {
// Show index content
res.render('index', {
title: 'Express'
});
};
- 编辑
app.js
文件,并用以下代码替换原始的index
路由app.use('/', routes);
:
app.get('/', index.show);
- 将控制器路径添加到
app.js
文件中var swig = require('swig');
声明之后,用以下代码替换原始代码:
// Inject index controller
var index = require('./controllers/index');
- 现在是时候检查一切是否如预期般进行了:我们将运行应用程序并检查结果。在您的终端/ shell 中键入以下命令:
npm start
检查以下网址:http://localhost:3000
,您将看到 express 框架的欢迎消息。
删除默认路由文件夹
让我们删除默认的routes
文件夹:
-
删除
routes
文件夹及其内容。 -
从
app.js
中删除user route
,在索引控制器行之后。
为头部和页脚添加部分文件
现在让我们添加头部和页脚文件:
- 在
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">
- 在
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>
- 现在,是时候使用
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
。
- 打开终端/Shell 并键入:
npm install -g sequelize-cli
- 使用以下命令安装
sequelize
:
npm install sequelize -save
提示
记住我们总是使用-save
标志将模块添加到我们的package.json
文件中。
- 在根文件夹上创建一个名为
.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')
}
- 在终端/Shell 上,键入以下命令:
sequelize init
- 在
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
命令创建了许多文件,包括数据库配置文件。该文件具有应用程序数据库的示例配置。
- 打开
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');
}
};
创建乐队模式
让我们创建一个模式,将在数据库中存储用户在系统中创建的每个乐队的数据。
- 打开终端/Shell 并键入以下命令:
sequelize model:create --name Band --attributes "name:string,
description:string, album:string, year:string, UserId:integer"
- 与上一步一样,创建了两个文件,一个用于迁移数据,另一个用作乐队模型,如下所示的代码:
'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/
。
- 打开
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;
};
- 打开
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 控制台之前,请确保它正在运行。要检查:
- 打开终端/ shell 并使用以下命令登录您的 Mysql:
mysql -u root
- 请记住,如果您使用不同的用户名或密码,请使用以下命令并将
youruser
和yourpassword
替换为您自己的凭据:
mysql -u youruser -p yourpassword
- 现在让我们创建我们的数据库,输入以下命令:
CREATE DATABASE mvc_mysql_app;
- 命令执行后的结果将是以下行:
Query OK, 1 row affected (0,04 sec)
这证实了操作是成功的,我们准备继续前进。
使用数据库迁移在 Mysql 上插入数据
现在是将模式迁移到数据库的时候了。再次使用sequelize-cli
进行此迁移。在继续之前,我们需要手动安装一个 Mysql 模块。
- 打开终端/ shell 并输入以下命令:
npm install
提示
请注意,Sequelize
接口取决于应用程序中使用的每种类型数据库的各个模块,我们的情况下是使用 Mysql
- 打开您的终端/ shell 并输入以下命令:
sequelize db:migrate
- 这将是上述操作的结果,您终端的输出:
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
创建了表,如我们在以下图中所见:
- 这张图片显示了左侧选择的乐队表,右侧显示了我们在乐队模式上设置的属性的内容:
乐队表
- 这张图片显示了左侧选择的
SequelizeMeta
表,右侧显示了config/migrations
文件夹中生成的Sequelize
文件的内容:
迁移文件
- 这张图片显示了左侧选择的用户表,右侧显示了我们在用户模式上设置的属性的内容:
用户表
SquelizeMeta
表以与我们在迁移文件夹中的迁移文件相同的方式保存迁移文件。
现在我们已经为数据库中的数据插入创建了必要的文件,我们准备继续创建应用程序的其他文件。
创建应用程序控制器
下一步是为模型 User 和 Band 创建控件:
- 在
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
。
- 在
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);
});
}
- 现在让我们重构
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
});
};
请注意,使用前面的代码,我们只是创建了一个简单的列表,以在主屏幕上显示一些专辑。
创建应用程序模板/视图
现在让我们创建应用程序视图:
- 在
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 %}
- 打开
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 %}
- 打开
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
视图添加路由,以及它们各自的控制器:
- 打开
app.js
并在索引控制器导入后添加以下行:
// Inject band controller
var bands = require('./controllers/band');
// Inject user controller
var users = require('./controllers/user');
- 在索引路由
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);
此时,我们几乎完成了应用程序的所有工作; 让我们在浏览器上检查结果。
- 打开您的终端/ shell,并键入以下命令:
npm start
- 打开浏览器并转到此 URL:
http://localhost:3000/
结果将是以下截图:
主屏幕的索引模板
如果我们检查http://localhost:3000/bands
上的 Band 路由,我们将看到一个空屏幕,http://localhost:3000/users
也是一样,但在这里我们找到了一个空的JSON数组。
让我们为 Band 的路由添加一些内容。
添加数据库内容
让我们在数据库中添加一些内容:
- 创建一个名为
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 */;
-
打开Sequel Pro,单击文件 > 导入 >,然后选择 SQL 文件
mvc_mysql_app.sql
。 -
返回浏览器并刷新
http://localhost:3000/bands
页面; 您将看到以下结果:
Band-list.html
创建一个乐队表单
现在我们将使用模态功能 bootstrap 创建乐队创建表单:
- 打开
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">×</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>
- 重新启动应用程序,打开您的终端/ shell,并键入以下命令:
npm start
- 单击插入专辑按钮,您可以在模型窗口内看到乐队表单,如下图所示:
模态屏幕
插入新乐队
现在让我们检查表单的行为:
- 使用以下数据填写表单:
-
名称:Sepultura
-
描述:巴西垃圾金属乐队
-
最佳专辑:https://s2.vagalume.com/sepultura/discografia/roots-W320.jpg
-
年份:1996
- 单击保存更改按钮。
表单处理后,您将被重定向到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 易于处理异步事件。 更明确地说,让我们看一下以下代码来比较这两个中间件:
- 从上一章的
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);
});
}
});
- 现在使用
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
文件是应用程序的核心。创建必要文件的步骤如下:
-
创建一个名为
chapter-03
的文件夹。 -
创建一个名为
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"
}
}
添加基线配置文件
现在让我们向项目添加一些有用的文件:
- 创建一个名为
.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
- 创建一个名为
.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 作为源代码控制。虽然这个文件不是运行应用程序所必需的,但我们强烈建议您使用源代码版本控制系统。
- 创建一个名为
app.js
的文件。
添加服务器文件夹
要完成应用程序的基本创建,我们现在将创建存储控件、模板和应用程序其他文件的目录:
- 创建一个名为
public
的文件夹,并在其中创建以下文件夹:
-
/images
-
/javascripts
-
/stylesheets
-
/uploads
-
/视频
- 创建一个名为
server
的文件夹,并在其中创建这些文件夹:
-
/config
-
/controllers
-
/models
-
/views
-
此时,我们的项目已经具有了所有基本目录和文件;让我们从
package.json
中安装 Node 模块。 -
在项目根目录打开您的终端/ shell,并输入以下命令:
npm install
在执行步骤 1、2 和 3 之后,项目文件夹将具有以下结构:
文件夹结构
让我们开始创建app.js
文件内容。
配置 app.js 文件
我们将逐步创建app.js
文件;它将与第一章
中创建的应用程序有许多相似的部分。但是,在本章中,我们将使用不同的模块和不同的方式来创建应用程序控件。
在Node.js
中使用 Express 框架,有不同的方法来实现相同的目标:
- 打开项目根目录下的
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
的更多信息。
- 在
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
。
- 在
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 进行身份验证。
- 现在让我们设置模板引擎和与应用程序数据库的连接。在
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
和用户会话。
- 在上一个代码块之后添加以下行:
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());
现在让我们添加所有应用程序路由。我们本可以使用外部文件来存储所有路由,但是我们将其保留在此文件中,因为我们的应用程序中不会有太多路由。
- 在
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
函数并配置我们的应用程序将使用的服务器端口
。
- 在上一个代码块之后添加以下代码:
// 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
中创建控制器文件:
- 创建一个名为
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');
};
- 创建一个名为
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');
};
- 创建一个名为
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 的完整文档。
-
创建一个名为
images.js
的文件。 -
添加以下代码:
// 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'];
- 在
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
文件夹中的目录的函数。
- 在上一个代码块之后添加以下行:
// 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');
});
});
};
添加检查用户是否经过身份验证并被授权插入图像的函数。
- 在文件末尾添加以下代码:
// Images authorization middleware
exports.hasAuthorization = function(req, res, next) {
if (req.isAuthenticated())
return next();
res.redirect('/login');
};
- 现在让我们重复这个过程来控制
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 的应用程序,我们将保持相同类型的配置:
- 在
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);
- 在
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);
- 然后,在
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);
- 接下来,在
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
文件,并将这些路由添加到应用程序菜单中:
- 在
views/partials
文件夹内创建一个名为footer.ejs
的文件,并添加以下代码:
<footer class="footer">
<div class="container">
<span>© 2016\. Node-Express-MVC-Multimedia-App</span>
</div>
</footer>
- 然后在
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 -->
- 在
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">×</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
类型,并创建一个循环来显示添加到图库中的所有图像。
- 在
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">×</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
文件夹中的内容:
- 复制以下文件夹及其内容,并将它们粘贴到
chapter-03
根项目文件夹中:
-
public/images
-
public/javascripts
-
bootstrap.min.js
-
jquery.min.js
-
public/stylesheets
-
bootstrap.min.css
-
style.css
-
style.css.map
-
style.sass
-
在
public
文件夹内创建一个名为uploads
的文件夹。 -
然后,在
public
文件夹内创建一个名为videos
的文件夹。
使用上传表单在应用程序中插入图像
现在是测试应用程序的时候了,需要注意的是,为此您应该启动您的 MongoDB。否则,应用程序在连接时会返回失败错误:
- 在项目根目录打开您的终端/Shell,并输入以下命令:
npm start
- 转到
http://localhost:3000/signup
并输入以下数据:
-
姓名:约翰
-
电子邮件:john@doe.com
-
密码:123
- 转到
http://localhost:3000/images-gallery
,点击图像上传按钮,填写表单标题并选择图像(请注意,我们设置了图像大小限制为1MB,仅用于示例目的)。您将看到一个模型表单,如下截图所示:
图像上传表单
- 选择图像后,点击保存更改按钮,完成!您将在
http://localhost:3000/images-gallery
页面看到以下截图:
图库图像屏幕
使用上传表单在应用程序中插入视频文件
与插入图像到我们的应用程序一样,让我们按照相同的步骤来插入视频:
- 转到
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
的基础上进行了大量的改进。
以下是我们安装的步骤:
- 打开终端/ shell 并输入:
npm install -g generator-express
-
创建一个名为
chapter04
的文件夹。 -
在
chapter04
文件夹中打开您的终端/ shell,并输入以下命令:
yo express
现在,按照以下顺序填写问题:
-
选择
N
,我们已经在步骤 2中创建了项目文件夹 -
选择
MVC
作为应用程序类型 -
选择
Swig
作为模板引擎 -
选择
None
作为 CSS 预处理器 -
选择
None
作为数据库(在本章后面,我们将手动设置数据库) -
选择
Gulp
进行 LiveReload 和其他内容
提示
不要担心Gulp
,如果你从未听说过它。在本书的后面,我们将看到并解释一些构建工具。
在生成器的最后,我们有以下目录结构:
应用程序文件夹结构
更改应用程序结构
与我们在第一章中使用的示例不同,使用 MVC 设计模式构建类似 Twitter 的应用程序,我们不会对当前的结构进行重大更改;我们只会更改views
文件夹。
作为示例应用程序,将有一本图片书;我们将在views
文件夹中添加一个名为 book 的文件夹:
-
在
app/views
文件夹中创建一个名为book
的文件夹。 -
现在我们将为 Cloudinary 服务创建一个配置文件。在本章后面,我们将讨论有关 Cloudinary 的所有细节;现在,只需创建一个新文件。
-
在根文件夹中创建一个名为
.env
的文件。
现在,我们有必要的基础来继续前进。
添加处理图像和 Cloudinary 云服务的 Node 模块
现在我们将在package.json
文件中为我们的应用程序添加必要的模块。
- 将以下突出显示的代码行添加到
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 书库下载示例文件。
-
在
controllers
文件夹中创建一个名为books.js
的文件。 -
将以下代码添加到
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都有非常相似的操作。
让我们看看如何为书籍对象创建模型:
- 在
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/
。
- 用以下代码替换
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
文件夹进行一些小改动并添加一些文件:
- 首先,让我们编辑
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 %}
- 创建一个名为
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 %}
- 然后创建一个名为
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 %}
- 创建一个名为
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 服务的更多信息:
- 转到
cloudinary.com/users/register/free
并注册一个免费帐户。
提示
在注册表单的末尾,您可以为您的云设置一个名称。我们选择了n6b
(Node.js 6 蓝图);选择您自己的名称。
- 从您的帐户中复制数据(
环境变量
)并将其直接放到仪表板面板上,如下面的屏幕截图所示:
Cloudinary 仪表板面板
- 现在在
.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
。
运行应用程序
现在是时候执行应用程序并上传一些照片了:
- 打开您的终端/Shell 并输入以下命令:
npm start
- 转到
http://localhost:3000/books
,您将看到以下屏幕:
书籍屏幕
上传和显示图像
现在让我们插入一些图像,并检查我们应用程序的行为:
- 转到
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.
-
让我们检查一下我们的 MongoDB,看看在我们继续之前发生了什么。
-
打开您的
RoboMongo
并选择第一个对象:
来自 MongoDB(RoboMongo)的屏幕截图
提示
请注意,您必须从左侧面板菜单中选择正确的数据库。
-
当我们上传一张图片时,API 会返回一个包含与该图片相关的所有信息的 JSON。我们将这个 JSON 存储为我们的书籍模型的图像属性,存储在 MongoDB 中,正如我们在之前的图片中所看到的。
-
通过Sample02至Sample06重复步骤 1。
检查 MongoDb 图片集合
让我们在 MongoDB 上看一下图片集合:
-
打开 RoboMongo 并从左侧面板中选择正确的数据库。
-
打开
collections
文件夹,双击图片集合。 -
在面板的右上方,单击
以表格模式查看结果
图标。
现在您可以在RoboMongo界面上看到以下屏幕截图:
来自图片集合的屏幕截图
在 Cloudinary 仪表板中创建文件夹
如前所述,我们设置了文件夹(folder: req.body.category
)。在这种情况下,文件夹名称将是类别名称。为了更好地在云中组织我们的图像,就像我们以编程方式做的那样,我们需要直接在 Cloudinary 仪表板中创建它们:
-
登录到您的 Cloudinary 帐户。
创建文件夹截图
注意
不要担心 Cloudinary 仪表板上的其他图像;它们是每个帐户中的默认图像。如果您愿意,可以删除它们。
-
点击右侧的输入字段(文件夹名称)并创建一个名为
animals
的文件夹。 -
点击右侧的输入字段(文件夹名称)并创建一个名为
cities
的文件夹。 -
点击右侧的输入字段(文件夹名称)并创建一个名为
nature
的文件夹。
您可以在图像顶部看到所有创建的类别,如下面的截图所示:
类别截图
现在当您选择一个类别时,您只会看到属于该类别的图像,例如animals
,如下图所示:
动物文件夹的截图
这是一种更有效的组织所有照片的方式,您可以创建几个相册,例如:
my-vacations/germany/berlin
road-trip/2015/route-66
URL 转换渲染
作为 Cloudinary API 的一部分,我们可以通过使用 URL 参数设置来操纵图像,就像我们在书籍页面上所做的那样:
-
转到
http://localhost:3000/books
。 -
打开您的 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 生成原始图像链接,而不应用任何转换:
- 打开
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>
- 现在当我们点击
链接到原始图像
时,我们可以在另一个浏览器窗口中看到完整的图像:
带有原始图像链接的书籍页面截图
重要的是要注意,我们还使用了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
相同的版本,不要拍照,创造它-为摄影师设计的应用程序。这次,我们不需要任何额外的模块来完成我们的任务:
-
创建一个名为
chapter05
的文件夹。 -
在
chapter05
文件夹中打开您的终端/ shell 并输入以下命令:
yo express
提示
请注意,我们已经在第四章中安装了generator-express
。
- 现在,按照以下顺序填写问题:
-
选择
N
:我们已经创建了一个文件夹 -
选择
MVC
:作为应用程序类型 -
选择
Swig
:作为模板引擎 -
选择
None
:作为 CSS 预处理器 -
选择
MongoDb
:作为数据库 -
选择
Gulp
:用于 LiveReload 和其他功能
提示
如果您从未听说过Gulp
,不要担心;在本书的后面,我们将看到并解释一些构建工具。
重构默认结构
正如我们所知,并且正如我们之前所做的,我们需要对我们的应用程序结构进行一些调整,以使其更具可扩展性并遵循我们的 MVC 模式:
-
在
app/views
文件夹中,创建一个名为pages
的文件夹。 -
在
app/views
文件夹中,创建一个名为partials
的文件夹。 -
将所有文件从
views
文件夹移动到pages
文件夹。
为页脚和页眉创建部分视图
现在,作为最佳实践,让我们为页脚和页眉创建一些部分文件:
-
在
app/view/partials/
中创建一个名为footer.html
的文件。 -
在
app/view/partials/
中创建一个名为head.html
的文件。
将 Swig 模板设置为使用 HTML 扩展名
正如您所看到的,我们使用了.html
文件扩展名,与之前的示例不同,我们使用了.swig
文件扩展名。因此,我们需要更改 express app.engine
配置文件,以便使用这种类型的扩展名:
-
从
app/config/
中打开express.js
文件。 -
替换以下突出显示的代码行:
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
文件扩展名。
创建部分文件
现在是时候创建部分文件本身了:
- 从
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.css
的CSS
框架和 Google 地图 API 链接:
- 从
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
创建的模板文件的内容:
- 打开
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
模板中。在本章的后面,我们将解释这个文件发生了什么。
- 打开
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>
- 打开
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 文件来创建一个显示用户精确位置的地图:
-
创建一个名为
getCurrentPosition.js
的文件,并将其保存在public/js
文件夹中。 -
将以下代码放入
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/
时,我们可以在地图上看到我们的地址指出,就像以下屏幕截图中一样:
启用地理定位的主屏幕
提示
请注意,您的浏览器将请求权限以跟踪您的位置
创建应用程序控制器
现在的下一步是创建应用程序控制器:
- 在
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 代码集成的地方:
-
创建一个名为
locations.html
的文件,并将其保存在app/views/pages/
文件夹中。 -
将以下代码放入
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 的屏幕截图
接下来最重要的代码是:
- 在
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 %}
您可以看到上一个代码的每一行都有一个注释;这样很容易理解每一行发生了什么。
-
让我们创建一个新文件。创建一个名为
add-location.html
的文件,并将其保存在app/views/pages/
文件夹中。 -
将以下代码放入
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 面板一次加载所有记录。
- 在项目根文件夹打开终端/Shell,并输入以下命令:
gulp
提示
请注意,在执行上述操作之前,您必须确保您的 MongoDB 已经启动。
- 转到
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
点击提交按钮,您将在地图上方看到一个成功消息。
- 现在我们将使用 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]
}]
)
-
在 RoboMongo 界面上,选择左侧面板上的 maps-api-development 数据库。
-
将代码粘贴到 RoboMongo 界面中:
RoboMongo 界面终端的截图
-
让我们来检查结果:双击左侧菜单上的locations集合。
-
在 RoboMongo 视图的右侧,点击以表格模式查看结果;您将看到以下结果:
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 中检查我们的位置集合:
-
打开你的 RoboMongo,并在左侧面板上选择maps-api-development数据库。
-
双击locations集合,您将看到以下数据:
索引之前的位置集合截图
您会注意到我们只有一个带有id索引的文件夹;这是 MongoDB 的默认设置。
- 复制以下代码并粘贴到 RoboMongo 界面中:
db.locations.ensureIndex({ 'coordinates' : '2dsphere'})
- 点击右上角菜单栏中的播放按钮。
结果将如下截图所示:
ensure.index()后的截图
请注意,现在我们已经创建了 2dsphere 索引。
检查地理位置应用
现在是测试应用程序的时候了。我们已经在我们的数据库中创建了八条记录,已经使用 ensure.index() MongoDB 对所有位置进行了索引,我们已经可以在地图中看到所有点的渲染,就像下面的截图中所看到的那样:
locations.html 的截图
在上一个屏幕截图中,您可能会注意到地图上的点彼此之间相距较远,这能够显示当我们改变距离搜索字段时所显示的点之间的距离差异。
在这个例子中,我们可以在搜索栏中插入任何纬度和经度,但我们只是固定这个字段来说明应用程序的地理定位功能。
当我们首次访问位置路由时,我们会显示数据库中的所有记录,就像我们在上一个屏幕截图中看到的那样。
让我们改变 locations.html 表单上的距离,看看会发生什么;转到 http://localhost:3000/locations,在距离字段中选择2km,然后点击提交按钮。
在 MongoDB 中使用\(near 和\)geometry 函数进行新查询的结果将如下所示:
通过 2km 筛选的位置页面的屏幕截图
这对于商店定位应用程序来说是非常有用的信息,但我们无法看到我们正在寻找的最近点在哪里。为了方便查看,我们将在地图上的左侧添加一个点列表,按从最近到最远的顺序列出。
按距离排序点
让我们添加一些代码行,使我们的搜索更直观:
- 在 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 存储库上下载完整的代码。
- 在{% 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 框架:
- 打开您的终端/Shell 并键入以下命令:
npm install strongloop -g
- 打开您的终端/Shell 并键入以下命令:
slc loopback
-
输入名称:目录选项为 chapter-06。
-
选择 empty-server(一个没有任何内容的 LoopBack API)
配置模型或数据源)选项。
不要担心输出的结尾,我们将在下一个主题中解释这一点。
结果将是以下文件夹和文件的结构:
文件夹和文件的屏幕截图
结构非常简单;几乎所有 LoopBack 的配置都在 JSON 文件中,如component-config.json,config.json,datasources.json,以及server文件夹中的所有其他文件。
提示
您可以通过在终端窗口中键入以下命令来了解有关slc命令行的更多信息:slc -help。
使用命令行创建模型
此时,我们已经有了开始开发 API 所需的结构。
现在我们将使用命令行来创建应用程序的模型。我们将构建两个模型:一个用于产品/摩托车,另一个用于用户/消费者。
- 在 chapter-06 文件夹中打开终端/Shell 并键入以下命令:
slc loopback:model
- 填写摩托车模型的以下信息,如下图所示:
创建摩托车模型后的终端输出的屏幕截图
- 填写属性名称:
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]:
- 让我们创建客户模型。打开终端/Shell 并键入以下命令:
slc loopback:model
- 填写审查模型的信息,如下图所示:
创建模型审查后的终端输出的屏幕截图
- 填写属性名称:
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 创建数据源:
- 在根项目中打开终端/ shell 并输入以下命令:
slc loopback:datasource
- 使用以下信息填写选项:
数据源终端输出的屏幕截图
请注意,最终选项是安装 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 格式。
- 打开终端/ shell 并输入以下命令:
npm start
API Explorer 的屏幕截图
可以看到 API 基本 URL 和 API 版本,我们的项目名称和应用程序端点。
- 当我们点击review模型时,我们可以看到所有带有 HTTP 动词的端点,如下图所示:
评论端点和 HTTP 动词的屏幕截图
创建的端点如下:
当然,您也可以直接使用浏览器访问它们。
重要的是要注意 GET 和 POST 端点是相同的,区别在于:当我们想要检索内容时,我们使用 GET 方法,当我们想要插入内容时,我们使用 POST 方法,PUT 和 DELETE 也是一样,我们需要在 URL 的末尾传递 ID,如 http://localhost:3000/api/reviews/23214。
我们还可以看到每个端点右侧有一个简要描述其目的的描述。
它还具有一些其他非常有用的端点,如下图所示:
评论端点的附加方法的屏幕截图
使用端点插入记录
现在我们将使用 API Explorer 界面向数据库中插入一条记录。我们将插入一个产品,即我们的摩托车:
-
转到 http://localhost:3000/explorer/#!/motorcycle/motorcycle_create。
-
将以下内容放入数据值字段中,然后点击“尝试一下”按钮:
{
"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 界面从数据库中检索记录。我们将使用摩托车端点:
-
转到 http://localhost:3000/explorer/#!/motorcycle/motorcycle_find。
-
单击“尝试一下”按钮,我们将得到与之前截图相同的结果。
请注意,我们正在使用 API 资源管理器,但我们所有的 API 端点都通过 http://localhost:3000/api/公开。
摩托车端点的屏幕截图
提示
请注意,我们正在使用一个名为JSON VIEW的 Chrome 扩展程序,您可以在这里获取:chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc
。
在处理大型 JSON 文件时非常有用。
添加数据库关系
现在我们已经配置了端点,我们需要在应用程序模型之间创建关系。
我们的反馈将被插入到特定类型的产品中,例如我们的摩托车示例,然后每个摩托车型号都可以接收各种反馈。让我们看看如何通过直接编辑源代码来创建模型之间的关系有多简单:
- 打开 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": {}
}
- 重新启动应用程序,打开终端窗口,然后输入以下命令:
npm start
我们可以看到 LoopBack 已经为这种关系创建了新的端点,如下图所示:
新端点创建的屏幕截图
现在我们可以使用以下方式获取与摩托车模型相关的所有反馈:
http://localhost:3000/api/motorcycles/
我们还可以通过简单地将评论 ID 添加到以下 URL 中来获取一个评论:
http://localhost:3000/api/motorcycles/
处理 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 内容添加到客户端
-
将 server/boot 中的 root.js 文件更改为 _root.js。
-
打开 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": {}
}
}
- 在 client 文件夹中创建一个名为 index.html 的新文件,并将其保存在 client 文件夹中。
现在我们配置应用程序以映射客户端文件夹并使其公开访问。这与我们为 Express 框架设置静态路由时非常相似。我们可以以其他方式设置应用程序的路由,但在此示例中,让我们保持这种方式。
添加 Bootstrap 框架和 React 库
现在让我们将依赖项添加到我们的 HTML 文件中;我们将使用 Bootstrap 和 React.js。
请注意,突出显示的文件是从内容传送网络(CDN)提供的,但如果您愿意,您可以将这些文件存储在 client 文件夹或用于 CSS 和 JavaScript 的子目录中:
- 打开新创建的 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/
。
- 在 client 文件夹中,创建一个名为 images 的新文件夹。
您可以将摩托车示例图像复制并粘贴到此文件夹中。此外,您可以在 Packt Publishing 网站和书籍的官方 GitHub 存储库中下载所有示例代码。
创建 React 组件
类似于 jQuery 小部件和 AgularJS 指令,有 React.js,这是一个非常有用的库,用于创建界面组件。但是,它不像 AngularJS 或 Ember.js 那样是一个完整的框架。
思考 React.js 的方式是通过思考界面组件:一切都是一个组件,一个组件可能由一个或多个组件组成。
请参阅以下图:
模拟 React.js 组件的屏幕截图
让我们逐个创建组件,以便更好地理解:
-
在 client 文件夹中,创建一个名为 js 的新文件夹。
-
在 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>
);
}
});
这是列表项组件。
- 现在让我们添加 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 函数:
- 现在让我们将 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>
);
}
});
- 现在我们添加 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>
);
}
});
- 最后,我们只需要创建一个 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>
);
}
现在我们对摩托车组件做同样的操作:
- 在 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>
);
}
});
- 让我们添加 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>
);
}
});
- 让我们创建一个 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 组件
- 在上一行之后添加以下行:
ReactDOM.render(
<MotorcycleBox api="/api/motorcycles"/>,
document.getElementById('motorcycle')
);
此渲染方法告诉在 HTML 摩托车 div 标签内渲染 MotorcycleBox 组件:
。创建新的反馈
现在是时候使用我们构建的应用程序创建新的反馈了:
- 打开终端/Shell 并输入以下命令:
npm start
- 转到 http://localhost:3000/,填写以下数据并点击提交按钮:
-
姓名:约翰·多
-
电子邮件:john@doe.com
-
反馈:很棒的红白经典摩托车!
结果会立即显示在屏幕上,如下截图所示。
新创建的反馈的屏幕截图
简单检查端点
让我们对我们的 API 进行简单的检查。前面的图像显示了特定型号摩托车的四条反馈;我们可以看到在图像中出现了评论的计数,但我们的 API 有一个端点显示这些数据。
转到 http://localhost:3000/api/reviews/count,我们可以看到以下结果:
{
count: 4
}
禁用远程 LoopBack 端点
默认情况下,LoopBack 创建了许多额外的端点,而不仅仅是传统的 CRUD 操作。我们之前看到了这一点,包括前面的例子。但有时,我们不需要通过 API 资源公开所有端点。
让我们看看如何使用几行代码来减少端点的数量:
- 打开 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);
};
- 重新启动应用程序,打开您的终端/Shell,并输入以下命令:
npm start
- 转到 http://localhost:3000/explorer/,点击review模型。
结果将如下图所示,只有 CRUD 端点:
评论端点的屏幕截图
提示
您可以在以下链接找到有关隐藏和显示端点的更多信息: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 文件:
-
创建一个名为 chapter-07 的文件夹。
-
在 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的内容。
- 打开终端/Shell 并输入以下命令:
npm install
-
创建一个名为 public 的文件夹。
-
创建一个名为 routes 的文件夹。
-
创建一个名为 views 的文件夹。
在这个阶段,你的文件夹必须具有以下结构:
chapter-01
node_modules
public
routes
views
package.json
添加配置文件
点文件在所有 Web 应用程序中都很常见;这些文件负责各种任务,包括版本控制和文本编辑器配置的配置,以及许多其他任务。
让我们为 Bower 包管理器添加我们的第一个配置文件(更多信息:bower.io/
):
- 创建一个名为.bowerrc 的文件,并添加以下代码:
{
"directory": "public/components",
"json": "bower.json"
}
这个文件告诉 Bower 在 public/components 中安装所有应用程序组件;否则,它们将被安装在根应用程序文件夹中。
- 创建一个名为.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。正如它的名字所示,它用于告诉版本控制应忽略哪些应用程序文件。
- 创建一个名为.gitignore 的文件,并添加以下代码:
node_modules/
public/components
.sass-cache
npm-debug.log
添加任务管理器文件
任务管理器在我们的应用程序中执行特定的任务。在第九章中,使用 Node.js 和 NPM 构建前端流程,我们将深入探讨它们在 Node.js 应用程序中的利用,但现在我们专注于文件本身:
- 创建一个名为 bower.json 的文件,并添加以下代码行:
{
"name": "chapter-07",
"version": "0.0.1",
"ignore": [
"**/.*",
"node_modules",
"components"
]
}
这是非常简单的代码,但这个文件和服务器端的 package.json 一样重要。Bower 将是前端任务管理器。在本章中,我们将看到如何使用它。接下来是 Gulp 文件。
提示
您可以在官方网站上找到有关 Gulp 文件的更多信息:gulpjs.com/
- 创建一个名为 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 用来存储关于项目的重要信息,比如安装过程、依赖关系和代码示例等。
- 创建一个名为 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 扩展名创建新文件。
- 创建一个名为 error.ejs 的新文件,并添加以下代码:
<% include header %>
<div class="container">
<h1><%- error.status %></h1>
<h4><%- message %></h4>
<p><%- error.stack %></p>
</div>
<% include footer %>
- 创建一个名为 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 文件;我们将在下面创建这个文件。
- 创建一个名为 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>
- 创建一个名为 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>© 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 之类的前端框架:
- 打开你的终端/Shell 并输入以下命令:
npm install bower -g
前面的命令在你的机器上全局安装了 Bower。
- 在根项目文件夹中,输入以下命令:
bower install bootstrap#v4.0.0-alpha
前面的命令将在 public/components 文件夹中安装 Bootstrap,正如我们在下面的图片中所看到的:
组件文件夹的屏幕截图
请注意,前面的命令也会添加 jQuery,因为 Bootstrap 依赖于 jQuery 库。让我们在 header.ejs 和 footer.ejs 中添加链接:
- 打开 views/header.ejs 并添加以下代码:
<link rel="stylesheet" href="components/bootstrap/dist/css
/bootstrap.min.css">
- 打开 footer.ejs 并添加以下代码:
<script src="img/jquery.min.js"></script>
<script src="img/bootstrap.min.js">
</script>
添加一些 CSS
现在让我们插入一些 CSS 代码来美化我们的示例页面:
-
在 public/css 中创建一个名为 style.css 的新文件。
-
将以下代码添加到 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;
}
}
我们对样式表进行了一些修改,以获得我们想要的书籍示例结果。
在这个阶段,我们有了主屏幕。
- 打开你的终端/Shell 并输入以下命令:
gulp
主屏幕的屏幕截图
添加实时重新加载插件
如前所述,我们将使用 livereload 插件。这个插件负责在我们更改应用程序文件时更新浏览器。现在让我们看看如何在我们的示例中实现它:
- 请记住,我们在本章的开头创建了 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
。
- 打开 views/header.ejs 并在样式表链接之后添加以下代码:
<% if (ENV_DEVELOPMENT) { %>
<script src="img/livereload.js"></script>
<% } %>
这些代码告诉应用程序在使用开发环境时注入 livereload 插件。
- 现在每次更改文件时,我们可以在终端上看到以下消息:
终端屏幕截图,带有 livereload 消息
- 但请记住,我们配置了 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 库来协助完成这项任务,因为我们已经在项目中使用了该库:
-
在 public/js 文件夹中,创建一个名为 main.js 的新文件。
-
将以下代码放入 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);
});
})();
让我们运行应用程序,看看终端上会发生什么。
- 在根项目上打开终端/ shell,并输入以下命令:
gulp
您的终端输出将如下所示:
应用程序运行时的输出终端屏幕截图
在这里,我们可以看到我们只有一个连接。但是,如果我们在新的浏览器窗口中打开 http://localhost:3000/,甚至在另一个浏览器中打开,我们可以看到两个连接,依此类推。
启动聊天应用程序
现在我们可以同时在两个窗口中使用我们的应用程序:
- 打开您的终端/ shell,并输入以下命令:
gulp
- 转到 http://localhost:3000/,输入名称John Doe,您将看到以下结果:
John Doe 用户的屏幕截图
我们可以看到只有一个用户,现在让我们用相同的 socket 打开另一个连接。使用一个新窗口或另一个浏览器。
- 转到 http://localhost:3000/,并输入名称Max Smith。您应该在右侧面板上看到以下结果:
用户面板的屏幕截图
现在我们有两个用户。让我们开始交谈...
- 在John Doe屏幕上,输入此消息:有人在吗?
检查Max Smith屏幕,您将看到John的消息出现,就像下面的图片中所示的那样:
Max Smith 屏幕聊天的屏幕截图
- 返回到 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
创建脚手架应用程序
现在是时候创建一个新文件夹并开始开发我们的博客应用程序了:
-
创建一个名为 chapter-08 的文件夹。
-
在 chapter-08 文件夹中打开您的终端/Shell,并输入以下命令:
yo keystone
在此命令之后,keystone.js 将触发一系列关于应用程序基本配置的问题;您必须回答这些问题,如下截图所示:
Keystone 生成器的提示问题
- 在所有生成器任务结束后,我们可以在终端窗口上看到以下输出:
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 对象名称
- 在项目根目录中打开 gulpfile.js 并删除有关 lint 任务的行:
watch:lint
- 修复管理用户名,打开根文件夹中的 Keystone.js 文件并替换以下代码:
keystone.set('nav', {
posts: ['posts', 'post-categories'],
galleries: 'galleries',
enquiries: 'enquiries',
userAdmins: 'user-admins'
});
就这些了,我们已经有了我们的博客。让我们来检查一下结果。
运行 Keystone 博客
- 打开终端/Shell 并输入以下命令:
gulp
Keystone 主页
如前所述,界面非常简单。它可以查看生成器生成的默认信息,包括有关用户和密码的信息。
- 点击右上角的登录链接,并使用上一个截图中的用户名和密码填写登录表单。结果将是控制面板,如下图所示:
Keystone 控制面板
每个链接都有一个表单,用于插入博客的数据,但现在不用担心这个;在本章后面,我们将看到如何使用管理面板。
正如我们在之前的图片中所看到的,布局非常简单。然而,这个框架的亮点不是它的视觉外观,而是它的核心引擎构建强大应用程序的能力。
提示
您可以在官方网站keystonejs.com/
上了解更多关于 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 的主题。
-
复制页面内容。
-
在 public/styles/boostrap/bootstrap 中,创建一个名为 _theme_variables.scss 的新文件,并粘贴从 Bootswatch 页面复制的代码。
-
打开 public/styles/bootstrap/_bootstrap.scss 并替换以下行:
// Core variables and mixins
@import "bootstrap/_theme_variables";
@import "bootstrap/mixins";
现在我们将重复步骤 1和2,但现在使用不同的 URL。
-
复制页面内容。
-
在 public/styles/bootstrap 中创建一个名为 _bootswatch.scss 的文件,并粘贴内容。
-
打开 public/styles/bootstrap/_bootstrap.scss 并替换以下突出显示的行:
// Bootswatch overhide classes
@import "bootswatch";
- 完成。现在我们有了一个与 keystone.js 采用的标准布局不同的布局,让我们看看结果。打开您的终端/Shell 并输入以下命令:
gulp
Keystone 主屏幕
通过这个小改变,我们已经可以看到所取得的结果。然而,这是一个非常表面的定制,因为我们没有改变任何 HTML 标记文件。
在之前的图片中,我们可以看到我们只是改变了页面的颜色,因为它保持了标记不变,只使用了一个 bootstrap 主题。
在下一个示例中,我们将看到如何修改应用程序的整个结构。
修改 KeystoneJS 核心模板路径
现在让我们对模板目录进行一些重构。
-
在模板中,创建一个名为 default 的文件夹。
-
将模板文件夹中的所有文件移动到新的 default 文件夹中。
-
复制默认文件夹中的所有内容,并将它们粘贴到一个名为 newBlog 的新文件夹中。
结果将是以下截图,但我们需要更改 keystone.js 文件以配置新文件夹:
模板文件夹结构
- 从根文件夹打开 keystone.js 文件并更新以下行:
'views': 'templates/themes/newBlog/views',
'emails': 'templates/themes/newBlog/emails',
完成。我们已经创建了一个文件夹来保存所有我们的主题。
构建我们自己的主题
现在我们将更改主题标记。这意味着我们将编辑 newBlog 主题内的所有 HTML 文件。我们使用github.com/BlackrockDigital/startbootstrap-clean-blog
提供的免费模板作为参考和来源。我们的目标是拥有类似以下截图的布局:
Keystone 主屏幕
- 打开模板/主题/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'>
- 删除{# HEADER #}和{# JAVASCRIPT #}注释之间的所有行。
提示
请注意,此操作将删除 default.swig 文件底部的 body 标记后的所有内容和 JavaScript 链接。
- 现在将以下代码行放在{# 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>
- 打开模板/主题/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 %}
- 打开模板/主题/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 %}
- 打开模板/主题/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 %}
- 打开模板/主题/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 →</a>
</li>
</ul>
{% endif %}
</div>
</div>
</div>
{% endblock %}
请注意,在 index.swig 中,我们添加了一些代码行以在索引页面上显示帖子列表,因此我们需要更改 index.js 控制器。
- 打开 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');
};
- 打开模板/主题/newBlog/views/post.swig 并用以下代码替换代码:
{% extends "../layouts/default.swig" %}
{% block content %}
<article>
<div class="container">
<a href="/blog">← 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。
-
打开 public/styles/site/_layout.scss 并使用代码。
-
在 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 文件。
- 打开 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";
- 将样本图像 header-bg-1290x1140.jpg 从 sample-images 文件夹添加到 public/images/文件夹中(您可以从 Packt Publishing 网站或 GitHub 官方书页下载所有示例文件)。
添加画廊脚本
正如我们所看到的,默认的 Keystone.js 主题非常简单,只使用了 Bootstrap 框架。现在我们将使用一个名为 Fancybox 的 jQuery 插件来应用新的样式在我们的画廊中。
提示
您可以在官方网站fancybox.net/
上获取有关Fancybox的更多信息。
- 打开模板/主题/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 -%}
- 现在让我们将以下代码行添加到模板/主题/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 依赖于它,所以我们不需要再次插入它。
- 打开您的终端/Shell 并输入以下命令:
gulp
模板图库
请注意,我们已经将示例内容包含到我们的博客中,但不用担心;在本章的后面,我们将看到如何包含内容。
扩展 keystone.js 核心
现在我们几乎准备好了新主题。
我们现在将看到如何扩展核心 keystone.js 并在我们的博客上添加另一页,如上一个截图所示,我们有一个关于菜单项,所以让我们创建它:
- 在 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();
- 将新模块添加到管理导航中,打开根文件夹中的 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 集合。
- 让我们自定义列显示。在 About.js 文件的 register()函数之前添加以下代码行:
About.defaultColumns = 'title, description|60%';
- 要将路由添加到关于页面,打开 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 函数创建控制器。
- 在 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');
};
- 在 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 控制面板
请注意,我们可以在上一个截图中看到关于菜单。
使用控制面板插入内容
经过所有这些步骤,我们成功为我们的博客创建了一个完全定制的布局;现在我们将使用书籍源代码下载中的 sample-images 文件夹中的可用图像输入内容:
-
转到 http://localhost:3000/keystone,使用用户:john@doe.com 和密码:123456 访问控制面板。
-
单击创建帖子类别按钮,将旧车标题插入输入字段,并单击创建按钮。
-
对于书籍示例,我们将只使用一个类别,但在实际应用中,您可以创建任意多个。
-
转到 http://localhost:3000/keystone/posts,单击创建帖子按钮,并按照以下截图中显示的内容添加内容:
创建帖子屏幕上的示例内容
-
对于第二个帖子条目,重复步骤 4的相同过程,并将标题更改为不带图像的示例帖子示例 II。
-
对于第三个帖子条目,重复步骤 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 框架的一些有趣特性,使用命令行:
- 打开终端/ shell,并输入以下命令:
slc loopback
-
将应用程序命名为 chapter-09。
-
选择 empty-server(一个没有配置模型或数据源的空 LoopBack API),然后按 Enter。
现在我们已经创建了应用程序脚手架。不用担心终端输出建议的下一个命令,因为我们将在本书的后面讨论这些命令。
提示
您可以在此链接阅读有关Loopback CLI的更多信息:docs.strongloop.com/display/public/LB/Command-line+reference
。
向项目添加数据源
在我们像在第六章中所做的那样创建模型之前,这次我们将首先添加数据源。这是因为我们使用命令行来创建整个项目。这意味着我们不手动编辑任何文件。
当我们使用命令行时,一个好的做法是先创建数据源,而不是应用程序模型。这个过程可以防止需要手动编辑文件来连接模型和数据源应用程序:
- 在终端/ shell 中,转到 chapter-09 文件夹,并输入以下命令:
slc loopback:datasource
- 按照以下截图中显示的问题进行填写:
数据源设置
默认情况下,如果我们在本地使用 MongoDB,就不需要设置用户名和密码。现在不用担心这个问题,但以后我们会看到如何更改配置以部署应用程序。如果你愿意,你也可以在本地环境中添加用户名和密码。
创建应用程序模型
现在让我们创建应用程序模型;对于这个示例,我们使用了两个模型:
- 在 chapter-09 文件夹中打开终端/ shell,并输入以下命令:
slc loopback:model
使用模型名称 gallery。
- 按照以下截图中显示的问题填写:
画廊模型设置
在第二个属性之后,按Enter**完成模型创建。
- 在 chapter-09 文件夹中打开终端/Shell,并输入以下命令:
slc loopback:model
使用模型名称自行车。
- 按照以下截图中显示的问题填写:
自行车模型设置
在第三个属性之后,按Enter**完成模型创建。
提示
您可以在此链接找到有关模型创建的更多信息:docs.strongloop.com/display/public/LB/Model+generator
。
现在不要担心模型之间的关系,我们将在下一步中看到,只使用命令行。
在应用程序模型之间添加关系
让我们定义模型之间的关系;我们将使用两种类型的关系,即:
-
hasmany:一个画廊可以有很多辆自行车
-
belongsTo:一辆自行车可以有一个画廊
请记住,我们只是试图做一些有用的事情,而不是复杂的事情,以说明使用 NPM 的构建过程,请按照以下步骤进行:
- 在 chapter-09 文件夹中打开终端/Shell,并输入以下命令:
slc loopback:relation
- 选择自行车模型,并按照以下问题填写:
自行车模型关系
- 选择画廊模型,并使用以下信息填写问题:
画廊模型关系
所以让我们检查一下是否一切都写得正确。
- 打开 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": {}
}
- 打开 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 文件夹设置为静态站点:
-
将 server/boot/root.js 文件重命名为 server/boot/_root.js。
-
将以下突出显示的行添加到 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": {}
}
}
- 在./client 文件夹中,创建一个名为 index.html 的新文件,并添加以下内容:
<!DOCTYPE html>
<html>
<head><title>Bikes Gallery</title></head>
<body>
<h1>Hello Node 6 Blueprints!</h1>
</body>
</html>
现在是时候检查之前的更改,并在浏览器中查看最终结果了。
- 打开终端/Shell 并输入以下命令:
npm start
- 打开您喜欢的浏览器,转到 http://localhost:3000/。
您应该会看到Hello Node 6 Blueprints!消息。
我们还在 http://localhost:3000/api/bikes 和 http://localhost:3000/api/galleries 上有 Restful API。
现在我们将看到如何重组一些目录,以准备使用 NPM 构建任务在云中部署应用程序。
重构应用程序文件夹
我们的重构过程包括两个步骤。
首先,让我们为应用程序源文件创建一个目录,例如 JavaScript、SCSS 和图像文件。
在第二步中,我们将在 client 文件夹中创建一些目录来接收我们的脚本。
让我们为图像、库、脚本和 SCSS 文件创建源文件夹。
创建图像文件夹
在这个文件夹中,我们将在使用 imagemin-cli 进行优化技术处理之前存储图像。
-
在根项目内,创建一个名为 src 的文件夹。
-
在 src 文件夹中,创建一个名为 images 的文件夹。
-
在图像文件夹中,创建一个名为 gallery 的文件夹。
-
从 Packt 网站(www.packtpub.com)或 GitHub 上的官方书籍存储库下载第九章的示例图像文件,然后将图像粘贴到 gallery 文件夹中。
提示
您可以在此链接了解更多有关 imagemin cli 的信息:github.com/imagemin/imagemin-cli
。
创建 libraries 文件夹
libraries 文件夹将存储一些 jQuery 插件。在 src 文件夹内,创建一个名为 libs 的文件夹。
创建 scripts 文件夹
由于我们使用了 jQuery 和一些插件,我们需要编写一些代码来使用 jQuery 库;我们将使用这个文件夹来做到这一点:
-
在 src 文件夹内,创建一个名为 scripts 的文件夹。
-
在 src/scripts 文件夹内,创建一个名为 gallery.js 的文件,并添加以下代码:
(function (){
'use-strict'
//jQuery fancybox activation
$('.fancybox').fancybox({
padding : 0,
openEffect : 'elastic'
});
})();
在这个例子中,我们只使用了一个插件,但在大型应用程序中,使用多个插件是非常常见的;在这种情况下,我们会为每个功能有一个文件。
然后,为了提高应用程序的性能,我们建议将所有脚本合并成一个文件。
创建 SASS 文件夹
SASS 文件夹将存储 scss 文件。我们正在使用 Bootstrap 框架,对于这个例子,我们将使用 SASS 分离版本来设置 Bootstrap 框架;现在不用担心这个,因为在本章后面我们会看到如何获取这些文件:
-
在 src 文件夹内,创建一个名为 scss 的文件夹。
-
在 scss 文件夹内,创建一个名为 vendor 的文件夹。
安装 Bower
正如我们在之前的章节中看到的,我们将使用 Bower 来管理前端依赖关系:
- 打开终端/Shell 并输入以下命令:
npm install bower -g
-
创建一个名为.bowerrc 的文件并将其保存在根文件夹中。
-
将以下内容添加到.bowerrc 文件中:
{
"directory": "src/components",
"json": "bower.json"
}
- 打开终端/Shell 并输入以下命令:
bower init
- 按照以下截图中显示的问题填写:
设置 Bower.json
安装应用程序依赖关系
在这个例子中,我们只使用了一个 jQuery 插件加上 Bootstrap 框架,所以让我们首先使用 Bower CLI 来安装 Bootstrap:
- 打开终端/Shell 并输入以下命令:
bower install bootstrap#v4.0.0-alpha --save
只需打开 src/components 文件夹查看 Bootstrap 和 jQuery 文件夹。
- 现在我们将在图像库中安装 jQuery fancybox 插件。打开终端/Shell 并输入以下命令:
bower install fancybox --save
因此,此时 src 文件夹将具有以下结构:
-
components/
-
bootstrap/
-
fancybox/
-
jquery/
创建 scss 文件夹结构
现在让我们设置 scss 文件夹来编译 bootstrap.scss 文件:
-
打开 src/components/bootstrap 文件夹并复制 SCSS 文件夹中的所有内容。
-
将内容粘贴到 src/scss/vendor 文件夹内。
-
在 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 来创建我们应用程序的页面:
- 在客户端文件夹内,创建以下文件夹:
-
css/
-
images/gallery/
-
js/
-
js/libs/
-
js/scripts/
-
views/
创建所有这些文件夹后,客户端目录将如下截图所示:
客户端文件夹结构
添加应用程序视图
现在是时候创建应用程序视图文件夹来存储所有应用程序模板了:
- 在 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>
- 在 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>
- 打开 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 来构建我们的前端应用程序:
- 打开终端/Shell 并输入以下命令:
bower install angularjs#1.5.0 --save
- 打开终端/Shell 并输入以下命令:
bower install angular-resource#1.5.0 --save
- 打开终端/Shell 并输入以下命令:
bower install angular-ui-router --save
提示
您可以在此链接了解更多关于 AngularJS 的信息:docs.angularjs.org/api
。
创建 AngularJS 应用程序
最后,我们将创建 AngularJS 应用程序,所以请按照以下步骤进行:
- 在 client/js 文件夹中,创建一个名为 app.js 的新文件,并添加以下代码:
(function(){
'use strict';
angular
.module('bikesGallery', ['ui.router','lbServices']);
})();
现在不用担心 lbServices 依赖项;在本章后面,我们将看到如何使用 Loopback 框架构建的 AngularJS SDK 工具来创建此文件。
- 在 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;
}
})();
- 在 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'
});
}
})();
- 在 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 自动生成所有应用程序服务:
- 打开终端/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 上可用。
- 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"
},
- 要使用其中一种方法,我们只需要将工厂注入到 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 存储库下载整个示例代码。
向应用程序添加内容
您可以通过两种方式添加内容,第一种是使用应用程序创建的端点,第二种是使用迁移文件。
在接下来的几行中,我们将展示如何使用第二种选项;这可能是一个简短而有趣的过程,用于创建迁移文件:
- 在 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);
});
});
- 在 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)工具:
- 打开终端/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/
。
- 在根项目内,创建一个名为.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 云服务上创建一个免费帐户:
-
转到
signup.heroku.com/
并创建一个免费帐户。 -
在
toolbelt.heroku.com/
上为您的平台下载Heroku 工具包。 -
按照您的平台进行安装过程。
现在您必须在您的机器上安装 Heroku 工具包,以进行测试。
- 打开终端/Shell 并输入以下命令:
heroku --help
终端输出列出了使用Heroku CLI的所有可能操作。
提示
本书假设您的机器上已经安装了 git 源代码控制;如果您还没有安装,请查看此页面:git-scm.com/downloads
。
创建 Heroku 应用程序
现在我们将创建一个应用程序并将其发送到您新创建的 Heroku 帐户:
-
创建一个名为.Procfile 的文件并将其保存在根项目文件夹中。
-
将以下代码粘贴到.Procfile 文件中:
web: slc run
- 打开终端/Shell 并输入以下命令:
git init
上一个命令初始化了一个 git 存储库。
- git add 命令将所有文件添加到版本跟踪:
git add
- git commit 命令将所有文件发送到本地机器上的版本控制。
git commit -m "initial commit"
现在是时候登录您的 Heroku 帐户并将所有项目文件发送到 Heroku git 源代码控制。
- 打开终端/ shell 并输入以下命令:
heroku login
输入您的用户名和密码。
- 打开终端/ shell 并输入以下命令:
heroku apps:create --buildpack https://github.com/strongloop
/strongloop-buildpacks.git
上一个命令将使用 strongloop-buildpacks 来配置和部署 Loopback 应用程序。
创建一个 deploy.sh 文件
最后,我们将使用.sh 文件创建我们的部署任务:
-
在根文件夹中创建一个名为 bin 的文件夹。
-
在 bin 文件夹内,创建一个名为 deploy.sh 的文件。
-
将以下代码添加到 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
- 在 package.json 文件的所有任务的末尾添加以下代码行:
"scripts": {
...
"deploy": "./bin/deploy.sh"
}
现在,每当您进行一次提交并输入 npm run deploy 命令进行一次更改时,引擎将启动 deploy.sh 文件并将所有已提交的更改上传到 Heroku 云服务。
- 打开终端/ shell 并输入以下命令:
npm run deploy
- 如果您遇到权限错误,请执行以下操作。在 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/,您将看到以下结果:
Heroku 云服务上的应用程序
当您单击“自行车查看画廊”按钮时,您将看到自行车画廊,如下所示:
自行车画廊
此外,当您单击每辆自行车时,您将看到 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 解决方案的工作原理:
持续集成过程
-
将代码提交到存储库。
-
CI 界面构建应用程序。
-
执行测试。
-
如果所有测试都成功,代码将被部署。
创建基线应用程序
让我们开始构建应用程序。首先,我们将创建一个应用程序文件夹,并添加一些根文件,比如.gitignore、package.json、.env 等等。
添加根文件
-
创建一个名为 chapter-10 的文件夹。
-
在 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 中,我们将使用一些模块来为我们的应用程序编写测试。我们将在本书的后面看到如何做到这一点。
- 创建一个名为.env 的文件,并添加以下代码:
SESSION_SECRET='<SESSION_SECRET>'
#MONGODB='<>'
MONGODB='<MONGODB>'
不要担心之前的代码;我们将在本章后面使用环境变量在Heroku和Codeship上替换这段代码,并且我们还将配置此文件以使用 Docker 容器。
提示
出于安全原因,如果您正在进行商业项目,请不要将您的凭据上传到开源存储库;即使您有一个私人存储库,也建议您在生产中使用环境变量。
- 创建一个名为 Profile 的文件,并添加以下代码:
web: node server.js
正如我们在之前的章节中所看到的,这个文件负责使我们的应用程序在 Heroku 上运行。即使它不是强制性的,将它包含进来也是一个良好的做法。
另外,由于我们正在使用 git 源代码控制,将.gitignore 文件包含进来是一个良好的做法。
- 创建一个名为.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 的文件夹,用于存储所有应用程序配置文件。所以让我们创建一个。
-
在项目根目录下,创建一个名为 config 的新文件夹。
-
创建一个名为 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 的应用程序。
创建控制器文件夹和文件
由于我们正在构建一个简单的应用程序,我们只会有两个控制器,一个用于用户,另一个用于主页:
-
在根项目文件夹内,创建一个名为 controllers 的新文件夹。
-
创建一个名为 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('/');
});
};
现在我们已经完成了应用程序控制器。
创建模型文件夹和文件
-
在根项目文件夹内,创建一个名为 models 的文件夹。
-
创建一个名为 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 文件夹内。让我们创建文件夹和文件:
-
在根项目内,创建一个名为 public 的文件夹。
-
在 public 文件夹内,创建一个名为 css 的文件夹,在 css 文件夹内创建一个名为 vendor 的文件夹,在 vendor 文件夹内创建一个名为 bootstrap 的文件夹。
-
转到
github.com/twbs/bootstrap-sass/tree/master/assets/stylesheets/bootstrap
,复制所有内容,并粘贴到 public/css/vendor/bootstrap 文件夹内。 -
在 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 字体文件,让我们创建一个:
-
在 public 文件夹内,创建一个名为 fonts 的新文件夹。
-
转到
github.com/twbs/bootstrap-sass/tree/master/assets/fonts/bootstrap
,复制所有内容,并粘贴到 public/fonts 文件夹内。
创建 JavaScript 文件夹和文件
由于我们正在使用 Bootstrap 框架,我们需要一个文件夹来存放所有的 Bootstrap 字体文件,让我们创建一个:
-
在 public 文件夹内,创建一个名为 js 的新文件夹。
-
在 js 文件夹内,创建一个名为 lib 的新文件夹。
-
在 js/lib 内创建一个名为 bootstrap.js 的新文件。
-
转到
github.com/twbs/bootstrap-sass/blob/master/assets/javascripts/bootstrap.js
,复制所有内容,并粘贴到 public/js/lib/bootstrap.js 文件中。 -
在 js/lib 内,创建一个名为 jquery.js 的新文件。
-
转到
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 文件夹和文件
现在是时候创建应用程序模板文件了:
- 在 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 %}
- 在 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 %}
- 在 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 %}
- 在 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 文件:
- 在 views/partials 文件夹内,创建一个名为 footer.html 的新文件,并添加以下代码:
<footer>
<div class="container">
<p>Node.js 6 Blueprints Book © 2016\. All Rights Reserved.</p>
</div>
</footer>
- 在 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 运行测试:
-
在根项目文件夹内,创建一个名为 test 的新文件夹。
-
在 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。
- 用以下信息替换根项目文件夹中的.env 文件:
SESSION_SECRET="3454321234"
MONGODB="localhost"
请注意,稍后在本书中,我们将把此文件恢复为其原始状态。
- 在根项目文件夹中打开终端/ shell,并输入以下命令:
npm install
- 现在输入以下命令:
npm test
您应该在终端输出中看到以下结果:
测试后的终端输出
请注意,两个测试都通过了,左侧的测试描述旁边有一个绿色的勾号图标。
运行应用程序
现在是时候检查应用程序了:
- 打开终端并输入以下命令:
npm start
- 转到 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 账户:
-
前往
github.com/join
,填写表格,然后点击创建账户按钮。 -
选择免费无限公共存储库复选框,然后点击继续按钮。
-
在第三步,您必须回答三个问题或选择跳过此步骤;点击提交按钮。从这里,您可以阅读指南或开始一个项目。
提示
请注意,您需要在开始项目之前验证您的电子邮件。
- 点击开始项目按钮,并填写存储库名称,记住您可以在 GitHub 上使用这个名称,但您需要为 Heroku 和 Codeship 过程选择另一个名称。之后,您将看到以下截图:
GitHub 项目
在本章的后面,我们将看到如何初始化本地 GIT 存储库以及如何将源代码推送到 GitHub 存储库。
创建 Heroku 免费账户
在上一章中,我们使用 Heroku 工具包命令直接将应用程序部署到 Heroku。这一次,您可以使用我们在第九章中创建的相同账户,使用 Node.js 和 NPM 构建前端流程,或者在www.heroku.com/
创建一个新账户。
创建 MongoLab 免费沙盒账户
MongoLab 是使用 MongoDB 作为服务所需的云服务。它提供了一个免费的有限账户作为沙盒,所以我们可以用它来部署我们的项目:
- 继续到注册页面;之后,您将收到来自 MongoLab 的两封电子邮件,一封是欢迎消息,另一封是验证您的账户的链接,如果您还没有账户。
验证您的账户后,登录到仪表板时,您将看到以下截图:
MongoLab 欢迎界面
-
点击创建新按钮。
-
选择单节点选项卡。
-
从标准行面板中,选择沙盒的第一个复选框。
-
向下滚动到页面底部,插入数据库名称nb6,然后点击创建新的 mongodb 部署按钮。
在这五个步骤结束时,您应该看到以下屏幕:
在 MongoLab 创建的数据库
为数据库创建用户名和密码
现在是时候创建用户名和密码来保护我们在云上的数据库了:
- 点击数据库名称。
您将看到以下警告,建议您创建用户名和密码:
数据库警告:没有用户和密码
-
点击警告消息内的点击这里链接。
-
插入以下信息:
database username: nb6
database password: node6
- 点击创建用户按钮。
获取连接字符串
现在我们在 MongoLab 云服务上运行了一个 MongoDB 实例。这是我们将在本章后面使用的连接字符串:
mongodb://<user>:<password>@ds023074.mlab.com:23074/nb6
提示
您必须用自己的用户名和密码替换以前的代码。
初始化 git 存储库并推送到 GitHub
此时,我们将创建本地 git 存储库,然后将其上传到我们刚在 GitHub 上创建的账户:
- 在根应用程序文件夹中打开终端/Shell,然后输入以下命令:
git init
- 通过在终端/Shell 中输入以下命令,将远程存储库添加到项目中:
git remote add origin https://github.com/<your github account
name>/n6b.git
您必须在以前的代码中使用自己的 github 用户名。
- 通过在终端/Shell 中键入以下命令将所有项目文件添加到源代码控制:
git add .
- 通过在终端/Shell 中键入以下命令提交项目更改:
git commit -m "initial commit"
最后一个命令是将所有文件上传到我们之前创建的 GitHub 存储库。
- 在终端/Shell 中键入以下命令:
git push -u origin master
使用 Heroku 仪表板创建 Heroku 应用程序
这次,我们将看到另一种使用 Heroku 云服务创建项目的方法:
-
在 Heroku 仪表板上,单击新建按钮,然后单击创建新应用链接。
-
在应用程序输入名称字段中输入以下名称:chapter-10-yourname
-
点击创建应用按钮。
将 Heroku 应用程序链接到您的 git 存储库
现在我们需要设置我们的 Heroku 帐户以链接到我们的 github 帐户。所以让我们按照以下步骤进行:
-
在 Heroku 仪表板上,点击 chapter-10-yourname 项目名称。
-
点击设置选项卡,向下滚动页面到域,并复制 Heroku 域 URL:
chapter-10-yourname.herokuapp.com
提示
请注意,我们不能为所有应用程序使用相同的名称,因此您需要在 chapter-10 之后提供您的名称。
稍后我们将使用应用程序名称来配置 Codeship 部署管道,所以不要忘记它。
向 Heroku 添加环境变量
现在我们需要创建一些环境变量,以便在我们的公共 github 存储库中保护我们的数据库字符串安全:
-
在 Heroku 仪表板上,点击 chapter-10-yourname 项目名称。
-
点击设置选项卡。
-
在设置选项卡中,单击显示配置变量按钮。
-
添加您自己的变量,如以下屏幕截图所示。在左侧添加变量名称,在右侧添加值:
Heroku 环境变量
提示
请注意,您必须在 Codeship 配置项目上重复此过程。
创建 Codeship 免费帐户
Codeship 是一个用于持续集成(CI)工具的云服务。创建帐户非常简单:
-
转到
codeship.com/sessions/new
,并使用右上角的注册按钮。您可以使用 GitHub 或 Bitbucket 帐户;只需点击您偏好的按钮。由于我们使用 GitHub,我们将选择 GitHub。 -
单击授权应用程序按钮。
您应该会看到以下屏幕:
Codeship 仪表板
下一步是单击您托管代码的位置。在这种情况下,我们将单击GitHub图标,因此我们将看到以下屏幕:
Codeship 配置的第二步
-
复制并粘贴 GitHub 存储库 URL(https://github.com/
/n6b.git),该 URL 是在 GitHub 设置过程中创建的,并将其粘贴到存储库克隆 URL输入中,如前图所示。 -
点击连接按钮。
现在我们已经使用三种工具(GitHub、Codeship 和 Heroku)设置了开发环境。下一步是创建设置和测试命令,并将管道部署添加到 Codeship 仪表板。
向 Codeship 添加环境变量
现在让我们像我们在 Heroku 仪表板中所做的那样,向 Codeship 添加相同的变量:
-
转到
codeship.com/projects/
,并选择 chapter-10-yourname 项目。 -
点击右上角的项目设置链接,如图所示:
Codeship 仪表板中的项目设置菜单
-
点击环境变量链接。
-
添加会话和 MongoDB 变量和值,就像我们之前对 Heroku 环境变量的配置所做的那样,并点击保存配置按钮。
在 Codeship 项目配置中创建设置和测试命令
现在我们回到 codeship 控制面板,并为我们的应用程序配置测试和部署命令:
- 将以下代码粘贴到设置命令文本区域:
# 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
- 将以下代码粘贴到测试命令文本区域:
npm test
- 点击保存并转到仪表板按钮。
在 Heroku 上创建部署管道
好了,我们几乎到了;现在我们需要创建一个管道,将构建与我们在 Heroku 上的部署环境集成起来:
-
点击右上角的项目设置链接,然后点击部署链接,如图所示:
-
在输入分支名称输入框中,输入以下名称:master。
-
点击保存 Pipeline 设置按钮。
-
在添加新的部署管道选项卡中,选择Heroku横幅。
现在我们将按照这里显示的输入字段填写:
Codeship Heroku 部署配置面板
将 Heroku API 密钥添加到 Codeship
为了提供前面截图中所需的信息,我们需要按照以下步骤进行:
-
打开一个新的浏览器窗口,转到 Heroku 仪表板
id.heroku.com/login
。 -
点击右上角的图片,然后点击账户设置。
-
向下滚动页面以获取 API 密钥,并点击显示 API 密钥,如图所示:
显示 API 密钥 Heroku 仪表板
-
输入密码并复制 API 密钥。
-
返回 Codeship 浏览器窗口,并将密钥粘贴到Heroku API 密钥输入字段中。
-
命名您的应用程序:n6b-your-own-name。
-
点击保存部署按钮。
这一步完成了我们的持续集成。每当我们修改代码并将更改发送到 GitHub 或 Bitbucket 时,Codeship 将运行我们在本章前面创建 Node.js 应用程序时设置的代码和测试。
在测试结束时,如果一切正常,我们的代码将被发送到 Heroku,并且将在 http://chapter-10-yourname.herokuapp.com/上可用。
在 Codeship 仪表板上检查测试和部署步骤
在这一点上,我们已经有了设置测试和部署所需的命令,但是我们需要检查一切是否正确配置:
-
登录到您的账户。
-
在左上角,点击选择项目链接。
-
点击 n6b-your-own-name 项目名称,您将看到所有带有成功、运行或失败标志的提交。
当我们点击其中一个时,我们可以看到以下带有逐步过程的截图:
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 数据库链接起来:
- 在根文件夹内,创建一个名为 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 容器端口;这是一个环境变量。
- 在根文件夹内,创建一个名为 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 镜像
在这一点上,我们已经有了设置测试和部署所需的命令,但是我们需要检查一切是否正确配置:
- 为你的项目创建 Docker 镜像:
docker build -t <your docker user name>/<projectname> .
在终端的输出末尾,我们可以看到类似于这样的消息:成功构建 c3bbc61f92a6。现在让我们检查已经创建的镜像。
- 通过打开终端/Shell 并输入以下命令来检查图像:
docker images
准备和运行 Docker 镜像
现在让我们测试我们的 Docker 镜像。在我们继续之前,我们需要对我们的应用程序进行一些小改动:
- 打开根文件夹中的 server.js 文件,并替换以下代码:
mongoose.connect('mongodb://' + (process.env.DB_PORT_27017_TCP_ADDR
|| process.env.MONGODB) + '/<database name>');
- 现在打开.env 文件,并用以下行替换代码:
SESSION_SECRET=
'ae37a4318f1218302e16e1516e4144df8a273798b151ca06062c142bbfcc23bc'
MONGODB='localhost:27017'
提示
步骤 1 和步骤 2 使用本地凭据,与我们为部署所做的不同。因此,在配置 Heroku 和 Codeship 的环境变量后,从 GitHub 跟踪中删除.env 文件,但在本地机器上保留具有本地凭据的文件。
现在是时候从 Docker hub 获取一个 MongoDB 镜像了。
- 打开终端/Shell 并输入以下命令:
docker pull mongo
上一个命令将获取一个新的 MongoDB 镜像。你可以使用相同的命令从 Docker hub 获取任何镜像。
提示
你可以在这个链接上找到更多的图像:hub.docker.com/explore/
。
- 用以下命令启动一个新的名为 db 的 MongoDB 容器:
docker run -d --name db mongo
- 现在我们需要将一个容器链接到另一个;输入以下命令:
docker run -p 3000:3000 --link db:db <your docker user name>
/<projectname>
- 转到 http://localhost:3000,你将看到你的应用程序正在运行。它看起来和在你的机器上一样。
将项目图像上传到您的 Docker hub 帐户
现在,是时候将你的图像上传到 Docker hub,并让其他用户可以使用了。
提示
你可以在这个链接上阅读更多关于Docker hub的信息:docs.docker.com/docker-hub/
。
-
转到
cloud.docker.com/
并创建一个免费帐户。 -
确认你的电子邮件地址后,转到
cloud.docker.com
登录菜单。你将看到以下仪表板:
Docker hub 仪表板
当您点击存储库按钮时,您会发现它是空的。现在让我们将我们的 Docker 图像推送到 Docker hub。
- 打开终端/Shell 并输入以下命令:
docker login
输入您的凭据,这些是您在注册过程中创建的。
- 要将项目上传到 Docker hub,请在终端/Shell 上运行以下命令:
docker push <your docker user name>/<projectname>
- 返回到
cloud.docker.com/_/repository/list
,刷新页面,您将看到您的存储库已发布在 Docker hub 上。
Docker 是一个强大的工具,必须进一步探索,但从本章开始,我们已经有足够的知识来使用 Docker 容器构建 Node.js 应用程序与 MongoDB,这意味着您可以在任何机器上使用我们创建的容器。无论您使用什么平台,您只需要安装 Docker 并将图像拉到您的机器上。
您可以获取任何图像,并在命令行上开始使用它。
总结
到本章结束时,您应该能够使用我们目前可用的所有最现代化的技术和工具来构建和部署应用程序,从而创建令人惊叹的 Web 应用程序。
我们已经探索了构建应用程序所需的所有资源,使用持续交付和持续集成,结合 Git 源代码控制,GitHub,Codeship,Heroku 和 Docker。我们还看到了如何在 Heroku 云服务的生产环境和 Codeship 的测试和持续集成中使用环境变量。