本篇目的

在首页显示数据库内所有的用户,我们可以先完成前端,然后后端,反之亦然。我们现在就参照以下步骤实现吧:

  1. 在 MongoDB 中创建一个数据库并且添加一些用户
  2. 然后通过 Express 创建一个 API
  3. 最后使用 Angular 来调用 API 并显示这些数据

创建数据库

这儿我们需要用到一个免费的可视化工具,和 Navicat For MySQL 类似的“道具”。下载传送门:Robomongo,安装好以后继续下面的步骤:

  • 确保 MongoDB 服务启动后运行 Robomongo。

  • 点击 Create 创建连接

  • 创建数据库:如图操作

  • 填写完毕后点一下左下角的 Test,得到两个小绿勾说明连接测试成功,可以点 Save 了。如果失败,说明服务有异常,赶紧 Google 一下或留言。

  • 对着左上角的连接名称点击右键,Create Database,然后输入数据库名,就叫 ForUsers 吧。

  • 接着双击 ForUsers 再对 Collections 单击右键,选择 Create Collection 创建一个 Json 集合。这和关系型数据库里的“表”类似,表名我们就叫 users 吧。结果如图:
    Robomongo 创建数据库

  • 最后我们往这个表里增加一些用户数据,右键 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 生成的框架中,默认包含两个路由模块:indexusers

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

确保它们每行都使用相同的缩进级别,因为 JadeExpress 中的默认模板引擎)对空格非常敏感。Express Generator 生成的 Jade 视图使用两个空格字符进行缩进。所以,你需要遵循同样的规范,你不能在同一个视图中和制表符【Tab 缩进】混合使用。否则你运行时会得到一个错误。

这些脚本是什么? 第一个是主要的 Angular 框架,第二个(angular-resource)用于调用 RESTful API,第三个(angular-route)用于管理路由。通过路由,我们定义用户在浏览应用程序时所看到的内容。

接下来,在 public->javascripts 下创建一个名为 forusers.js 的新文件,我们将在这里编写所有的 JavaScript 代码。

layout.jadeAngular 脚本后面添加对 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 包括两个步骤:

  1. 首先,我们将 ng-app 属性添加到我们的 HTML 元素中。当 Angular 脚本被加载时,它会在 DOM 中寻找这个属性,如果找到它,它会 Hook 到你的应用程序中。
  2. 接下来,我们为我们的应用程序创建一个 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){
}]);

在我们的配置函数中,我们使用 $routeProviderwhen 方法来配置路由。

$routeProvider
    .when('/', {
        templateUrl: 'partials/home.html'
    })

第一个参数 / 是相对路径,第二个参数是一个对象,它指定视图的路径(通过 templateUrl)。我们可以有多个对 when 方法的调用,每次调用都指定一个不同的路由。最后,我们使用其它方法来指示如果用户导航到任何其它 URL 时,则应将其重定向到根路径 /

几乎要完成啦,我们只需要对主页的 Jade 视图进行一点小小的更改。所以,打开 views->index.jade,并更改此文件的内容如下:

extends layout

block content
  div(ng-view)

我删除了这个视图中的现有内容(Welcome to Express),而是添加了一个带有属性 ng-viewdiv。 此属性告诉 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 APIngRoute 用于管理路由。

接下来,在文件末尾键入以下代码以创建控制器:

app.controller('HomeCtrl', ['$scope', '$resource',
    function($scope, $resource) {}
]);

解释一下。这里我们使用应用程序模块的控制器方法来定义一个新的控制器。

第一个参数是一个字符串,指定此控制器的名称。按照惯例,我们将 CtrlController 添加到我们的 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>

我们使用 ulli 来渲染用户列表。 我们的 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 控制器进行处理。

我们已经完成了这部分,返回浏览器并刷新主页,你应该会看到用户列表:
完成!


文中翻译和慕课那篇译文有差异。
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐