使用 MEAN 进行全栈开发基础篇——3、接着前面玩儿查询
本篇目的 在首页显示数据库内所有的用户,方法多式多样,可以从前端到后端,反之亦可。我们现在就参照以下步骤实现吧:在 MongoDB 中创建一个数据库并且添加一些用户然后通过 Express 创建一个 API最后使用 Angular 来调用 API 并显示这些数据现在开始创建数据库 这儿我们需要用到一个免费的可视化工具,Just like Navicat For MySQL 这样的东西。下
本篇目的
在首页显示数据库内所有的用户,我们可以先完成前端,然后后端,反之亦然。我们现在就参照以下步骤实现吧:
- 在 MongoDB 中创建一个数据库并且添加一些用户
- 然后通过 Express 创建一个 API
- 最后使用 Angular 来调用 API 并显示这些数据
创建数据库
这儿我们需要用到一个免费的可视化工具,和 Navicat For MySQL 类似的“道具”。下载传送门:Robomongo,安装好以后继续下面的步骤:
-
确保 MongoDB 服务启动后运行 Robomongo。
-
创建数据库:
-
填写完毕后点一下左下角的 Test,得到两个小绿勾说明连接测试成功,可以点 Save 了。如果失败,说明服务有异常,赶紧 Google 一下或留言。
-
对着左上角的连接名称点击右键,Create Database,然后输入数据库名,就叫 ForUsers 吧。
-
接着双击 ForUsers 再对 Collections 单击右键,选择 Create Collection 创建一个
Json
集合。这和关系型数据库里的“表”类似,表名我们就叫users
吧。结果如图:
-
最后我们往这个表里增加一些用户数据,右键
users
,选择Insert Document
,弹出一个 Json 编辑器,想想一个用户一般包括哪些信息呢?姓名、年龄、性别,好,就这三个,参考如下。可以复制进去点击 Validate 进行验证,格式无误,会提示你JSON is valid!,这表明数据格式正确,此时可以点击Save
保存。
{
"name": "小明",
"age": 20,
"gender": "男"
}
{
"name": "小强",
"age": 22,
"gender": "男"
}
{
"name": "小张",
"age": 21,
"gender": "女"
}
- 现在双击
users
,就能在右边看到刚刚插入的数据了,上面的db.getCollection('users').find({})
为查询语句,和 MySQL 中的SELECT * FROM users;
类似。你会发现每个用户里都多出了一个_id
字段,这是 MongoDB 自动添加 ID。现在我们数据库创建好了,开始干正事!
通过 Express 创建一个 API
在这个步骤中,你将学习到 Node 的模块系统,Express 路由和通过 Monk 从 MongoDB 中获取数据。
我们首先在 VSCode 中打开 ForUsers 项目文件夹,然后在根目录打开 app.js
,文件中第 7 行前的第一部分包含了几个 require 函数调用,require
是 Node 中的内置方法,主要用来引用其它文件中定义的模块:
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');
第 8、9 行代码引入了整个框架的路由模块,1 个路由模块定义了 1 个或多个关联的端点(路由地址)以及对应的处理器。Express Generator 生成的框架中,默认包含两个路由模块:index 和 users
var index = require('./routes/index');
var users = require('./routes/users');
让我们来看一看 index 这个路由,routes->index.js,内容如下:
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
在第 1 行,我们在当前模块引用了 express,当使用 require 方法时,当前模块会依赖于 目标 模块。require 方法可能返回 1 个方法或者对象,类似于面向对象编程里的 new
,使得对象实例化。在这个例子中,这个 express 变量就是 1 个对象,它的内部提供了一个叫做 Router 的方法,我们在第 2 行就调用了这个方法,用来访问 express 中的路由对象。我们用 1 个路由来定义我们应用中的端点,我们在这些端点中接收请求,每个端点会被关联到一个路由处理程序,处理程序负责处理端点中接收到的请求。
“端点”即路由地址,也就是
localhost:3000/index
中的/index
部分。
现在看看下一行中路由配置的示例:
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
我们使用路由里的 get
方法来定义 1 个路由和它的处理程序。第 1 个参数就是端点;在这里,/
代表网站的根路径或者主页。第 2 个参数就是路由的处理程序。这里为一个匿名函数。
在 Express 中,所有的路由处理程序都具有相同的形参(形式参数)。第 1 个参数为请求对象,第 2 个为响应对象,第 3 个是当前引用链中的下一个处理程序。Express 使用了链接在一起的中间件函数。当使用 Express 构建中间件时,有时候你可能想要调用链中的下一个中间件函数。你可以使用 next 变量实现。但是当我们使用路由的时候,我们几乎不需要这个操作,所以在这里你可以安全地删除 next 变量。
现在,看看这个方法的主体。 res
变量表示响应对象,响应对象有很多有用的方法:
- render:渲染视图
- send:用以向客户端发送纯文本内容
- json:向客户端发送一个 Json 对象
- redirect:将客户端重定向到新的地址
这里,我们渲染 index 的视图,它在 views->index.jade 中已经定义好了。
这是路由的基本结构。 现在我们需要为我们的用户创建一个 RESTful API。我们将在类似 /api/users 这样的端点上展示我们的用户。
我们直接打开 routes->users.js 文件,开始编辑:
var express = require('express');
var router = express.Router();
// 连接数据库
var monk = require('monk');
var db = monk('localhost:27017/ForUsers');
router.get('/', function(req, res) {
var collection = db.get('users');
/*
* 还记得 Robomongo 里的那句 db.getCollection('users').find({}); 吗
* 同样的,db.get('表名'),然后再 .find({}) 表示查找所有
* 类似 MySQL 里的 SELECT * FROM users;
*/
collection.find({}, function(err, users) {
if (err) throw err;
res.json(users);
});
});
module.exports = router;
前两行和之前所说的相同,我们引入 Express 并获取路由对象。
接下来,我们引入 Monk,它是用于 MongoDB 的一个持久化模块。还有另一个流行的持久化模块用于与 Mongo 一起工作,名为 Mongoose,但在本教程中,我更喜欢使用 Monk,因为它比 Mongoose 更简单。
前面说过,根据模块的实现,require 方法可能返回 1 个对象或方法。这里,当我们导入 Monk 时,我们得到的是一个方法,而不是一个对象。所以,此处的 monk 变量是一个方法,我们调用以访问我们的数据库:
var db = monk('localhost:27017/ForUsers');
现在看看我们的路由处理程序如何实现。
function(req, res) {
var collection = db.get('users');
collection.find({}, function(err, users){
if (err) throw err;
res.json(users);
});
}
首先我们调用 db
对象的 get
方法,传递表的名称 users
。它返回一个集合对象,该集合对象提供了许多方法来处理其内容。
- insert
- find
- findOne
- update
- remove
在这里,我们使用 find
方法来获取结果集合中的所有用户,此方法的第一个参数是确定过滤的对象(字段)。由于我们想要得到所有用户的所有信息,所以我们传递一个空对象。第二个参数是当从数据库返回结果时执行的回调方法,此方法遵循 error-first(错误优先)模式,这是 Node 中回调方法的标准协议。使用此模式,回调方法的第一个参数为错误对象,第二个参数为结果(如果存在)。当你使用 Node 开发更多的应用程序时,你将了解更多的回调模式。
在这个回调中,首先我们检查 err
对象是否被设置,如果没有错误作为获取用户文档的一部分,err
将为 null
。我们在这里抛出错误将停止程序的执行,并向用户报告错误。如果没有错误,我们只需通过调用 res.json
方法返回一个 Json 对象。
最后,看看这个模块的最后一行:
module.exports = router;
在该行中,我们指定在另一个模块中依赖此模块时返回的对象(或方法)。这种情况下,我们在 Express 中返回路由对象。 所以,这个模块的目的是获取路由并注册路由,随后返回它。
还有一小步。我们此时构建了一个路由模块,但我们没有在任何地方使用。再次打开 app.js
,在顶部附近找到以下代码:
var index = require('./routes/index');
var users = require('./routes/users');
Oh,因为我们直接使用的 users.js
,这里面已经引入了这个文件,所以不必再写入,如果是自己创建的其它路由,就得添加进来。
我们已经引入了用户路由模块并将其存储在一个名为 users
的变量中。接下来,我们需要使用它,在 app.js
找到以下部分:
app.use('/', index);
app.use('/users', users);
把 app.use('/users', users);
修改为
app.use('/api/users', users);
好了,记得在 ForUsers
目录里运行 nodemon
,然后浏览器访问:http://127.0.0.1:3000/api/users
可以看到我们数据库里的用户都被显示出来啦:
Json 内容高亮的原因是 Chrome 扩展 —— Prism Pretty,推荐安装。
该扩展已下架,可通过下载安装:https://maxosky.lanzoui.com/id7S4pf93qd
Angular,上!
在这一步,你将学习 Angular 的基础知识。如果您已经熟悉 Angular,可以跳过解释,但请务必应用所有代码到项目中。
Angular 是构建 SPA(单页应用程序)的流行前端框架,它提供了路由、依赖注入、可测试性和遵循 MVC 架构模式的代码解耦。如果这一切听起来太 Geeky,不要担心,在本节中,你将看到大多数这样的功能。
首先,我们需要添加 Angular 到我们的应用程序。转到 views->layout.jade,并在 head
的末尾添加这三个引用:(我这里使用的 bootcss 镜像)
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular.min.js')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular-resource.min.js')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular-route.min.js')
现在你的 layout.jade 文件看起来应该是这样的:
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular.min.js')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular-resource.min.js')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular-route.min.js')
body
block content
确保它们每行都使用相同的缩进级别,因为 Jade(Express 中的默认模板引擎)对空格非常敏感。Express Generator 生成的 Jade 视图使用两个空格字符进行缩进。所以,你需要遵循同样的规范,你不能在同一个视图中和制表符【Tab 缩进】混合使用。否则你运行时会得到一个错误。
这些脚本是什么? 第一个是主要的 Angular 框架,第二个(angular-resource)用于调用 RESTful API,第三个(angular-route)用于管理路由。通过路由,我们定义用户在浏览应用程序时所看到的内容。
接下来,在 public->javascripts 下创建一个名为 forusers.js
的新文件,我们将在这里编写所有的 JavaScript 代码。
在 layout.jade
中 Angular 脚本后面添加对 forusers.js
的引用:
script(src='/javascripts/forusers.js')
再次强调,请确保此行与 head
中的其它脚本引用处于相同的缩进级别。此时的 layout.jade
应该是这样的:
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular.min.js')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular-resource.min.js')
script(src='//cdn.bootcss.com/angular.js/1.6.0/angular-route.min.js')
script(src='/javascripts/forusers.js')
body
block content
现在我们已经拥有了必要的脚本,现在是时候添加 Angular 到我们的应用程序,添加 Angular 包括两个步骤:
- 首先,我们将
ng-app
属性添加到我们的 HTML 元素中。当 Angular 脚本被加载时,它会在 DOM 中寻找这个属性,如果找到它,它会 Hook 到你的应用程序中。 - 接下来,我们为我们的应用程序创建一个 Angular 模块,Angular 应用由一个或多个模块组成。一个简单的应用程序,你只需要一个模块叫做
app
,但随着应用程序的增长,你可能希望将各种功能划分为不同的模块,以便更好地组织和维护。
在 VSCode 中继续打开 layout.jade
,将 ng-app
添加到顶部的 html
元素附近,就像这样:
doctype html
html(ng-app='ForUsers')
在 Jade 中,我们使用括号将属性添加到 HTML 标签。当这行由 Jade 模板引擎渲染时,我们将得到一个 HTML 元素,如下所示:
<html ng-app='ForUsers'>
我们为 ng-app
设置的值是应用程序模块的名称。现在我们需要创建这个模块。
打开 forusers.js
并写上这段代码:
var app = angular.module('ForUsers', []);
Angular 对象可用于全局空间,所以它无处不在。模块方法可用于定义新模块或获取对现有模块的引用。第一个参数是模块的名称,这是我们使用 ng-app
上面的名称。第二个参数是一个依赖关系数组,如果提供此参数,模块方法将定义一个新模块并返回一个引用,如果将其排除,它将尝试获取对现有模块的引用。这里,我们传递一个空数组,表示这个模块不依赖于任何其它模块。
这就是我们必须做的将 Angular 这货 Hook 到我们的应用程序。接下来,我们将使用 Angular 重新构建我们的主页,以显示数据库中的所有用户。
使用 Angular 重新构建首页
Express Generator 生成的默认应用程序使用 Jade 作为视图引擎,这些 Jade 视图在服务器上进行解析和渲染(呈现),并将 HTML 标签返回给客户端,很多 Web 框架都是这样工作的。但在这个应用程序中,我们将使用不同的构建风格。我们将返回纯 Json 对象,并让客户端(Angular)呈现视图,而不是将 HTML 标签返回到客户端。让我解释为什么:
早些时候,在“什么时候使用 Node”一节中,提到一个常见的情况,Node 擅长在文本数据库中构建 RESTful API,有了这种架构风格,我们没有任何数据转换的开销,我们在 Mongo 中存储 Json 对象,然后通过 RESTful API 得到它们,并直接在客户端(Angular 中)展示。 Json 是原生的 JavaScript 和 MongoDB 对象。因此,通过在整个堆栈中使用它,我们减少映射或将其转换为其它类型的开销。通过从我们的 API 返回 Json 对象并在客户端上渲染视图,可以提高性能和可扩展性,因为服务器的 CPU 不应浪费在为大量并发用户渲染视图上。此外,我们可以重复使用相同的 API 来构建另一个客户端,例如 iPhone 或 Android 应用。
在这个步骤中,我们将不使用 Jade 视图构建的默认主页,而是使用 Angular 视图来取代。
在 public 下创建一个新文件夹 partials。我们将在这里存储我们的所有视图,在此先在文件夹下创建一个名为 home.html 的新文件,在此文件中,只需写入:
<h1>Home Page</h1>
现在,当用户访问主页时,我们需要告诉 Angular 渲染此视图。我们使用 Angular 路由实现这个功能。
在 forusers.js
中,更改应用程序模块的声明如下:
var app = angular.module('ForUsers', ['ngRoute']);
我们在依赖关系数组中添加了一个对 ngRoute
模块的引用。ngRoute
是用于配置路由的内置 Angular 模块之一。
在应用程序模块声明之后的 forusers.js
内编写以下代码:
app.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'partials/home.html'
})
.otherwise({
redirectTo: '/'
});
}]);
讲解一下,我们使用应用程序模块的配置方法为我们的应用程序提供配置。这个代码将在 Angular 检测到 ng-app
并尝试启动时立即运行,config
方法需要一个数组:
app.config([]);
此数组可以具有零个或多个依赖关系,以及一个用于实现配置的函数。这里,我们对 $routeProvider
有一个依赖,它是一个在 ngRoute
模块中定义的服务。这就是为什么我们改变了我们的应用程序模块声明来依赖于 ngRoute
的原因。配置函数接收 $routeProvider
作为参数。
app.config(['$routeProvider', function($routeProvider){
}]);
在我们的配置函数中,我们使用 $routeProvider
的 when
方法来配置路由。
$routeProvider
.when('/', {
templateUrl: 'partials/home.html'
})
第一个参数 /
是相对路径,第二个参数是一个对象,它指定视图的路径(通过 templateUrl
)。我们可以有多个对 when
方法的调用,每次调用都指定一个不同的路由。最后,我们使用其它方法来指示如果用户导航到任何其它 URL 时,则应将其重定向到根路径 /
。
几乎要完成啦,我们只需要对主页的 Jade 视图进行一点小小的更改。所以,打开 views->index.jade,并更改此文件的内容如下:
extends layout
block content
div(ng-view)
我删除了这个视图中的现有内容(Welcome to Express),而是添加了一个带有属性 ng-view
的 div
。 此属性告诉 Angular 在哪里渲染视图。这样,当用户第一次点击主页时,我们的 Jade 视图将在服务器上渲染并返回给客户端。在实际应用中,此视图将具有网站的基本模板(例如,导航栏,Logo 等)。它还将有一个内容区域(由 ng-view
指示),用于 Angular 渲染视图。当用户浏览应用程序时, Angular 将用不同的 Angular 视图替换内容区域,这可以防止整页重载,从而提高性能。这就是为什么我们称这些应用程序为单页应用程序:一个页面从服务器下载后,任何后续页面都只是内容区域的替换。
注意:再次强调,Jade 对空格字符非常敏感。在同一视图中,不能混用空格和制表符缩进。Express Generator 生成的默认 Jade 视图使用两个空格字符缩进。所以,确保在 div(
ng-view
)之前添加两个空格字符,否则会导致运行时解析错误。
让我们在实现最后一步之前做一个快速测试。返回浏览器,访问 http://127.0.0.1:3000,应该看到我们的新主页用 Angular 构建,并且注意 URL 变成了:http://127.0.0.1:3000/#!/。那么,何以见得我们是使用了 Angular?只需要开启控制台:
实现控制器
主页已正确 Hooked,现在我们需要从数据库获取用户并在主页上呈现。在 Angular 或任何其它 MVC 中,这都是控制器的职责。视图纯粹负责渲染,而控制器负责获取视图的数据或处理从视图中产生的事件(例如点击按钮等)。
让我们为我们的主视图创建一个控制器。打开 forusers.js
并更改应用程序模块的声明如下:
var app = angular.module('ForUsers', ['ngResource', 'ngRoute']);
现在我们依赖于两个模块:ngResource
用于调用 RESTful API,ngRoute
用于管理路由。
接下来,在文件末尾键入以下代码以创建控制器:
app.controller('HomeCtrl', ['$scope', '$resource',
function($scope, $resource) {}
]);
解释一下。这里我们使用应用程序模块的控制器方法来定义一个新的控制器。
第一个参数是一个字符串,指定此控制器的名称。按照惯例,我们将 Ctrl 或 Controller 添加到我们的 Angular 控制器名称末尾。
第二个参数是数组。此数组可以包含零个或多个字符串,每个字符串表示此控制器的依赖关系。这里,我们指定了一个对 $scope
和 $resource
的依赖。这两个都是内置的 Angular 服务,这就是为什么他们前面加一个 $
符号。我们使用 $scope
将数据传递给视图, $resource
则用于使用 RESTful API。此依赖性数组中的最后一个对象是表示控制器的主体或实现的函数。在这个例子中,我们的函数获取两个参数:$scope
和 $resource
。这是因为我们在声明这个函数之前引用了 $scope
和 $resource
。
让我们实现这个控制器。在控制器功能内,键入以下代码:
app.controller('HomeCtrl', ['$scope', '$resource',
function($scope, $resource) {
var Users = $resource('/api/users');
Users.query(function(users) {
$scope.users = users;
});
}
]);
这里,我们调用 $resource
方法来获取给定 API 端点(/api/users
)的资源对象。此对象将提供与我们的 API 一起使用的方法。我们使用查询方法来获取所有用户。查询方法获得一个回调函数,当查询结果准备就绪时,它将被执行。此功能将接收从数据库检索的用户。最后,我们将这些用户存储在 $scope
中,以便我们可以在视图中访问时进行渲染。记住,$scope
是视图和控制器之间的胶水。
现在,我们需要改变视图来渲染视图列表。打开 partials->home.html 并添加如下代码:
<ul>
<li ng-repeat='user in users'>{{user.name}}</li>
</ul>
我们使用 ul
和 li
来渲染用户列表。 我们的 li
有一个称为 ng-repeat
的 Angular-specific 属性。 Angular 中的这些属性称为指令(或函数),用于向 HTML 元素添加行为。ng-repeat
的值是类似于 JavaScript 中的 forEach
表达式。因此,users
本质上就是我们在控制器中早期在 $
范围中设置的属性。users
中的 user 在此列表中表示一个用户。因此,这个 li
元素将根据 users 数组中的每个用户重复渲染。我们使用 {{}}
来编写表达式。在这里,我们只需在我们的 li
中渲染每个用户的名字。
最后,我们需要将此控制器注册为路由的一部分。 回到 forusers.js
中,更改路由配置如下:
.when('/', {
templateUrl: 'partials/home.html',
controller: 'HomeCtrl'
})
有了这个,我们告诉 Angular,当用户访问到网站的根路径时,显示 partials/home.html 静态文件并附加 HomeCtrl
控制器进行处理。
我们已经完成了这部分,返回浏览器并刷新主页,你应该会看到用户列表:
更多推荐
所有评论(0)