MEAN 栈开发 [翻译]

在众成翻译上看到了这篇文章 Create a Web App and RESTful API Server Using the MEAN Stack

我的翻译如下:

MEAN 堆栈是一种目前流行的 Web 开发堆栈。MEAN 所代表的含义即:MongoDB,Express,AngularJS 和 Node.js。MEAN 受到大量关注的原因在于它允许开发者在客户端和服务端同时使用 JavaScript。MEAN 创造出了一个基于 JSON 数据对象的近乎完美和谐的开发环境:MongoDB 负责存储类 JSON 形式的数据,Express 和 Node.js 则快速的实现基于 JSON 的请求创建,AngularJS 则保证了客户端可以流畅地收发 JSON 数据文件。

由于运行在客户端的 AngularJS 和 运行在服务端的 Express 是两种面向 Web APP 的框架,所以 MEAN 一般用于开发基于浏览器的 Web 应用。而 MEAN 的另一 个值得关注的应用方向则是开发 RESTful API 服务。如今我们开发的应用通常都需要考虑如何优雅地支持各类终端设备,比如各种移动手机和笔记本,因此创建 RESTful API 服务已经变得日益重要也越来越普遍。本文讨论的问题就是如何借助 MEAN 堆栈 去快速开发 RESTful API 服务。

AngularJS 作为一种客户端框架在创建 API 服务的时候并非必须。你当然可以写一个 Android 或 IOS APP 去测试 REST API,而在这篇文章中,我们则是选择用 AngularJS 去创建 Web APP,进而展示 APP是如何借助 REST API 服务运行的。

在这篇文章中我们将创建的 APP 是一个通讯录管理 APP,包括基本的增删改查读写更新操作。 首先,我们将创建一个 RESTful API 服务作为接口去对 MongoDB 数据库进行查询和保存数据。随后,我们利用 API 服务去创建一个基于 Angular 的 Web APP,以此提供 面向用户的接口。

这样接下来我们将重点说明一个 MEAN 应用的基本架构,至于一些例如身份验证、访问控制和数据鲁棒性验证之类的常见功能,我们将不作详述。

依赖配置

首先推荐阅读 Getting Started with Node.js on Heroku。当然如果你之前用 Node.js 开发过应用并发布到 Heroku 上,可以跳过这步。

确保在本地机器上成功安装过:

源代码结构

整个项目的源代码在 GitHub 上。 仓库文件包含:

  • package.json
    • 一个包含你的应用元数据的配置文件。当项目目录根存在 package.json 时, Heroku 将会使用 Node.js 进行构建打包。
  • app.json
    • 一个描述 web apps 的清单文件。该文件会声明环境变量、插件以及在 Heroku 上运行 app 需要的其他信息。当然,我们会创建一个 “Deploy to Heroku” 按钮。
  • server.js
    • 该文件包含所有服务端的代码,也即实现了我们的 REST API。该文件由 Node.js 编写,使用了 Express 框架并用 Node.js 驱动 MongoDB。
  • /public directory
    • 该目录包含所有的客户端文件,包含所有的 AngularJS 代码。

查看运行效果

你只需要点击下放按钮,就可以查看即将创建的 APP 的实际运行效果:

Deploy

现在,我们开始一步一步来

创建一个新 App

创建一个新项目目录,随后 cd 进入该目录。在这个目录里我们会创建一个运行在 Heroku 上的 app 用来运行你的代码,我们首先使用 Heroku CLI

Bash
    $ git init Initialized empty Git repository in /path/.git/ $ heroku create Creating app... done, stack is cedar-14 https://sleepy-citadel-45065.herokuapp.com/ | https://git.heroku.com/sleepy-citadel-45065.git

当你创建了一个 app,一个名叫 heroku 的 git 分支也随即创建,并且关联你本地的 git 仓库。Heroku 同样也会为你的 app 随机生成一个名称 (如本例中的 sleepy-citadel-45065)。

Heroku 通过根目录中的 package.json 文件来判断 app 是否为 Node.js 应用。因此我们创建一个 package.json 文件,内容如下:

JSON
    {
      "name": "MEAN",
      "version": "1.0.0",
      "description": "A MEAN app that allows users to manage contact lists",
      "main": "server.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node server.js"
      },
      "dependencies": {
        "body-parser": "^1.13.3",
        "express": "^4.13.3",
        "mongodb": "^2.1.6"
      }
    }

这个 package.json 文件确定了将使用的 Node.js 的版本,以及本项目需要安装的各种依赖。当这个 app 部署完成后,Heroku会根据这个 package.json 文件,通过 npm install 指令安装相应版本的 Node.js 和相关依赖文件。

为了保证你的系统可以在本地运行服务,在本地项目目录中执行下面的命令安装依赖:

Bash
    $ npm install

当所有依赖安装完成之后,你将可以在本地运行这个 app。

准备一个 MongoDB 数据库

在完成你的应用和文件目录的配置之后,接下来需要创建一个 MongoDB 实例来保存应用数据。我们将会使用一个部署在云端的数据库服务平台 mLab 来创建 MongoDB 数据库。

想要把 mLab 插件添加到应用中,需要在 Heroku CLI 上运行以下命令:

Bash
    $ heroku addons:create mongolab

数据库的链接 URI 将保存在 config var 中。接下来我们将会获取这个变量并在 Node.js 中定义为 process.env.MONGODB_URI

现在我们准备好数据库了,可以开始编码了。

通过Node.js连接数据库和 App 服务

目前 Node.js 针对 MongoDB 广泛使用的驱动有两种:官方的 Node.js driver 和 一个在 Node.js driver 基础上封装的 ODM(文件对象映射,类似于 SQL 的 ORM) Mongoose。 这两者各有各自的优势,本文我们会使用官方的 Node.js driver.

首先创建一个 server.js 文件,我们会在这个文件里创建一个新的 Express 应用并且连接 mLab 数据库,内容如下:

JavaScript
    var express = require("express");
    var path = require("path");
    var bodyParser = require("body-parser");
    var mongodb = require("mongodb");
    var ObjectID = mongodb.ObjectID;

    var CONTACTS_COLLECTION = "contacts";

    var app = express();
    app.use(express.static(__dirname + "/public"));
    app.use(bodyParser.json());

    // Create a database variable outside of the database connection callback to reuse the connection pool in your app.
    var db;

    // Connect to the database before starting the application server.
    mongodb.MongoClient.connect(process.env.MONGODB_URI, function (err, database) {
      if (err) {
        console.log(err);
        process.exit(1);
      }

      // Save database object from the callback for reuse.
      db = database;
      console.log("Database connection ready");

      // Initialize the app.
      var server = app.listen(process.env.PORT || 8080, function () {
        var port = server.address().port;
        console.log("App now running on port", port);
      });
    });

    // CONTACTS API ROUTES BELOW

连接数据库的时候,有以下几点需要注意:

  • 尽可能多地使用数据库连接池 connection pool 来管理应用资源. 我们在全局作用域中初始化 db 变量,这样所有的路由控制器都能访问到数据库连接。
  • 在数据库连接完成之后再初始化应用,这样能够保证应用在进行数据库操作时不会崩溃或抛出错误。

至此,app 和数据库已经连接了,接下来我们会来实现 RESTful API 服务。

使用 Node.js 和 Express 来创建 RESTful API

首先需要定义我们想要暴露的所有接口(或者说数据)。我们的通讯录列表 APP 将会允许所有的用户对于其联系人进行增删改查操作。因此我们的数据请求接口定义如下:

Bash
/contacts

| Method | Description |
| --- | --- |
| GET | Find all contacts |
| POST | Create a new contact |

/contacts/:id

| Method | Description |
| --- | --- |
| GET | Find a single contact by ID |
| PUT | Update entire contact document |
| DELETE | Delete a contact by ID |

接下来我们在 server.js 文件中添加路由请求处理:

JavaScript
    // CONTACTS API ROUTES BELOW

    // Generic error handler used by all endpoints.
    function handleError(res, reason, message, code) {
      console.log("ERROR: " + reason);
      res.status(code || 500).json({"error": message});
    }

    /*  "/contacts"
     *    GET: finds all contacts
     *    POST: creates a new contact
     */

    app.get("/contacts", function(req, res) {
    });

    app.post("/contacts", function(req, res) {
    });

    /*  "/contacts/:id"
     *    GET: find contact by id
     *    PUT: update contact by id
     *    DELETE: deletes contact by id
     */

    app.get("/contacts/:id", function(req, res) {
    });

    app.put("/contacts/:id", function(req, res) {
    });

    app.delete("/contacts/:id", function(req, res) {
    });

上面这部分代码搭建好了我们所定义的接口的初步框架。

实现 API 接口

接下来,我们添加数据库处理逻辑来具体实现每个接口。

首先实现 “/contacts” 请求下的 POST 接口。这个接口用来向数据库中创建和保存新的联系人。每个联系人对象将有如下的数据模型:

JSON
    {
      "_id": <ObjectId>
      "firstName": <string>,
      "lastName": <string>,
      "email": <string>,
      "phoneNumbers": {
        "mobile": <string>,
        "work": <string>
      },
      "twitterHandle": <string>,
      "addresses": {
        "home": <string>,
        "work": <string>
      }
    }

下面的代码实现 /contacts 的 POST 请求:

JavaScript
    app.post("/contacts", function(req, res) {
      var newContact = req.body;
      newContact.createDate = new Date();

      if (!(req.body.firstName || req.body.lastName)) {
        handleError(res, "Invalid user input", "Must provide a first or last name.", 400);
      }

      db.collection(CONTACTS_COLLECTION).insertOne(newContact, function(err, doc) {
        if (err) {
          handleError(res, err.message, "Failed to create new contact.");
        } else {
          res.status(201).json(doc.ops[0]);
        }
      });
    });

为了测试该请求和接口,我们作如下部署:

Bash
    $ git add package.json $ git add server.js $ git commit -m 'first commit' $ git push heroku master

接下来我们需要保证至少有一个应用实例正在运行:

Bash
    $ heroku ps:scale web=1

随后,使用 cURL 来进行一个 POST 请求:

Bash
    curl -H "Content-Type: application/json" -d '{"firstName":"Chris", "lastName": "Chang", "email": "support@mlab.com"}' http://your-app-name.herokuapp.com/contacts

到此我们还没创建web app,但是通过访问 mLab management portal已经可以看到我们成功地创建并保存到数据库中。新创建的联系人会保存在 MongoDB 的 “contacts” 集合中。

接下来继续完善 server.js , 实现剩下所有的接口:

JavaScript
    var express = require("express");
    var path = require("path");
    var bodyParser = require("body-parser");
    var mongodb = require("mongodb");
    var ObjectID = mongodb.ObjectID;

    var CONTACTS_COLLECTION = "contacts";

    var app = express();
    app.use(express.static(__dirname + "/public"));
    app.use(bodyParser.json());

    // Create a database variable outside of the database connection callback to reuse the connection pool in your app.
    var db;

    // Connect to the database before starting the application server.
    mongodb.MongoClient.connect(process.env.MONGODB_URI, function (err, database) {
      if (err) {
        console.log(err);
        process.exit(1);
      }

      // Save database object from the callback for reuse.
      db = database;
      console.log("Database connection ready");

      // Initialize the app.
      var server = app.listen(process.env.PORT || 8080, function () {
        var port = server.address().port;
        console.log("App now running on port", port);
      });
    });

    // CONTACTS API ROUTES BELOW

    // Generic error handler used by all endpoints.
    function handleError(res, reason, message, code) {
      console.log("ERROR: " + reason);
      res.status(code || 500).json({"error": message});
    }

    /*  "/contacts"
     *    GET: finds all contacts
     *    POST: creates a new contact
     */

    app.get("/contacts", function(req, res) {
      db.collection(CONTACTS_COLLECTION).find({}).toArray(function(err, docs) {
        if (err) {
          handleError(res, err.message, "Failed to get contacts.");
        } else {
          res.status(200).json(docs);
        }
      });
    });

    app.post("/contacts", function(req, res) {
      var newContact = req.body;
      newContact.createDate = new Date();

      if (!(req.body.firstName || req.body.lastName)) {
        handleError(res, "Invalid user input", "Must provide a first or last name.", 400);
      }

      db.collection(CONTACTS_COLLECTION).insertOne(newContact, function(err, doc) {
        if (err) {
          handleError(res, err.message, "Failed to create new contact.");
        } else {
          res.status(201).json(doc.ops[0]);
        }
      });
    });

    /*  "/contacts/:id"
     *    GET: find contact by id
     *    PUT: update contact by id
     *    DELETE: deletes contact by id
     */

    app.get("/contacts/:id", function(req, res) {
      db.collection(CONTACTS_COLLECTION).findOne({ _id: new ObjectID(req.params.id) }, function(err, doc) {
        if (err) {
          handleError(res, err.message, "Failed to get contact");
        } else {
          res.status(200).json(doc);
        }
      });
    });

    app.put("/contacts/:id", function(req, res) {
      var updateDoc = req.body;
      delete updateDoc._id;

      db.collection(CONTACTS_COLLECTION).updateOne({_id: new ObjectID(req.params.id)}, updateDoc, function(err, doc) {
        if (err) {
          handleError(res, err.message, "Failed to update contact");
        } else {
          res.status(204).end();
        }
      });
    });

    app.delete("/contacts/:id", function(req, res) {
      db.collection(CONTACTS_COLLECTION).deleteOne({_id: new ObjectID(req.params.id)}, function(err, result) {
        if (err) {
          handleError(res, err.message, "Failed to delete contact");
        } else {
          res.status(204).end();
        }
      });
    });

设置 web app 的静态文件

完成了请求 API 之后,我们将用它来创建浏览器可访问的 Web App。

首先在项目根目录下创建一个 public 目录。随后将 example app 的 public 目录 拷贝到该目录下。 这个目录包含所有的 HTML 模板文件 和 AngularJS 代码。

在 HTML 文件中,你会发现一些非常规的 HTML 代码,比如在 index.html 中的 “ng-view”,这些其实是 AngularJS 的指令:

HTML, XML
    <div class="container" ng-view>

使用模板可以让我们复用代码并且动态地生成相应的视图。

利用 AngularJS 构建 Web App

我们将会使用 AngularJS 把客户端的所有内容整合成一个 Web App,包括管理页面路由请求、渲染不同页面的视图、向后端发送数据和接收来自后端的数据。

我们的 AngularJS 代码位于 /public/js 目录下的 app.js 文件中。我们这里重点关注主页加载时的代码,也就是 (“/”) 路由请求时需要做的事。我们需要注意以下几点:

  • 用 AngularJS routeProvider 来分配路由、渲染正确的视图和模板。
  • 用 AngularJS service 来从数据库中获取联系人信息数据。
  • 用 AngularJS controller 来把数据转换到视图。

这部分的代码如下:

JavaScript
    angular.module("contactsApp", ['ngRoute'])
      .config(function($routeProvider) {
        $routeProvider
          .when("/", {
            templateUrl: "list.html",
            controller: "ListController",
            resolve: {
              contacts: function(Contacts) {
                  return Contacts.getContacts();
              }
            }
          })
      })
      .service("Contacts", function($http) {
        this.getContacts = function() {
          return $http.get("/contacts").
            then(function(response) {
                return response;
            }, function(response) {
                alert("Error retrieving contacts.");
            });
        }
      })
      .controller("ListController", function(contacts, $scope) {
        $scope.contacts = contacts.data;
      });

接下来,我们来关注每个部分是如何具体实现的。

用 AngularJS routeProvider 来分配路由

路由的配置写在 routeProvider 模块。

JavaScript
    angular.module("contactsApp", ['ngRoute'])
      .config(function($routeProvider) {
        $routeProvider
          .when("/", {
            templateUrl: "list.html",
            controller: "ListController",
            resolve: {
              contacts: function(Contacts) {
                  return Contacts.getContacts();
              }
            }
          })
      })

主页的路由有以下几个组件组成:

  • templateUrl 组件指定需要显示的模板。
  • Contacts 组件完成从 API 服务请求所有的联系人信息的工作。
  • ListController 组件让我们可以从视图中获取数据、向scope作用域中添加数据。

使用 AngularJS services 向 API server 进行请求

AngularJS service 会创建一个可以被不同请求访问的同一个对象。我们创建的服务则相当于客户端的一个容器,包含所有的 API 请求接口。

主页的路由中使用 getContacts 函数来请求联系人数据:

JavaScript
    .service("Contacts", function($http) {
      this.getContacts = function() {
        return $http.get("/contacts").
          then(function(response) {
            return response;
          }, function(response) {
            alert("Error retrieving contacts.");
          });
      }

我们的服务函数借用了 AngularJS $http 服务模块来创建一个 HTTP 请求。该模块同样也会返回一个 promise 对象,利用这个 promise 你可以修改或者增加其他功能(比如 logging)。

需要注意,在使用 $http 服务时,我们使用了相对路径(例如, “/contacts”)而不是绝对路径(例如,app-name.herokuapp.com/contacts)。

使用 AngularJS controllers 来扩展 scope 作用域参数

到此为止,我们已经配置好了路由、定义好了需要展示的模板、利用 “Contacts” 服务取到了数据。接下来我们需要创建一个控制器 controller 来整合整个过程。

JavaScript
    .controller("ListController", function(contacts, $scope) {
      $scope.contacts = contacts.data;
    })

我们的 controller 把服务端的联系人数据添加到 homepage 的 scope 作用域中,定义为变量 $scope.contacts。这样我们就可以在模板文件(比如 list.html)中直接获取这些数据。我们可以在模板中使用 AngularJS 的 ngRepeat directive对所有的contacts 数据进行迭代处理:

HTML, XML
    <div class="container">
      <table class="table table-hover">
        <tbody>
          <tr ng-repeat="contact in contacts | orderBy:'lastName'" style="cursor:pointer">
            <td>
              <a ng-href="#/contact/{{contact._id}}">{{ contact.firstName }} {{ contact.lastName }}</a>
            </td>
          </tr>
        </tbody>
      </table>
    </div>

完成整个项目

现在我们对于需要实现的 homepage 的路由处理已经有了较深的理解,其他页面的处理是类似的模式 /public/js/app.js file。这些模块都需要定义一个routeProvider、一个或多个服务函数来产生相应的 HTTP 请求,以及一个 controller 来扩展 scope 作用域参数。

在完成 AngularJS 的代码之后,再次部署 app:

Bash
    $ git add server.js $ git add public $ git commit -m 'second commit' $ git push heroku master

现在 web app 的所有组件都已经完成,你可以通过下面的命令打开 app 查看效果:

Bash
    $ heroku open

总结

在这篇文章中,我们重点提到了以下几点:

  • 使用 Express 和 Node.js 创建 RESTful API server。
  • 连接 MongoDB database 和 API server 来完成查询和保存数据。
  • 使用 AngularJS 穿件 web app。

我们希望你可以体会到使用 MEAN 堆栈进行 web 应用开发的威力。

一些注意事项

当你在 Heroku 上运行 MEAN 堆栈开发应用时,随着运行时间和数据量的增长,你需要注意优化和缩小项目体积。你可以参考 Optimizing Node.js Application Concurrency 这篇文章对你的项目进行优化. 想要升级优化你的数据库, 可以参考这篇文章 mLab add-on documentation

展望

如我们之前所说,我们忽略了一些在真是项目中需要关心的细节问题。事实上,我们并没有实现 user 用户模块,用户权限控制,或者输入表单验证之类的事情。而这些都是你接下来可以做的事情。同时如果你有任何问题,也可以发送邮件到 support@mlab.com

原文

Create a Web App and RESTful API Server Using the MEAN Stack