在众成翻译上看到了这篇文章 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
server.js
/public
directory你只需要点击下放按钮,就可以查看即将创建的 APP 的实际运行效果:
现在,我们开始一步一步来
创建一个新项目目录,随后 cd
进入该目录。在这个目录里我们会创建一个运行在 Heroku 上的 app 用来运行你的代码,我们首先使用 Heroku CLI:
$ 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
文件,内容如下:
{
"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 和相关依赖文件。
为了保证你的系统可以在本地运行服务,在本地项目目录中执行下面的命令安装依赖:
$ npm install
当所有依赖安装完成之后,你将可以在本地运行这个 app。
在完成你的应用和文件目录的配置之后,接下来需要创建一个 MongoDB 实例来保存应用数据。我们将会使用一个部署在云端的数据库服务平台 mLab 来创建 MongoDB 数据库。
想要把 mLab 插件添加到应用中,需要在 Heroku CLI 上运行以下命令:
$ heroku addons:create mongolab
数据库的链接 URI 将保存在 config var 中。接下来我们将会获取这个变量并在 Node.js 中定义为 process.env.MONGODB_URI
。
现在我们准备好数据库了,可以开始编码了。
目前 Node.js 针对 MongoDB 广泛使用的驱动有两种:官方的 Node.js driver 和 一个在 Node.js driver 基础上封装的 ODM(文件对象映射,类似于 SQL 的 ORM) Mongoose。 这两者各有各自的优势,本文我们会使用官方的 Node.js driver.
首先创建一个 server.js
文件,我们会在这个文件里创建一个新的 Express 应用并且连接 mLab 数据库,内容如下:
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
连接数据库的时候,有以下几点需要注意:
db
变量,这样所有的路由控制器都能访问到数据库连接。至此,app 和数据库已经连接了,接下来我们会来实现 RESTful API 服务。
首先需要定义我们想要暴露的所有接口(或者说数据)。我们的通讯录列表 APP 将会允许所有的用户对于其联系人进行增删改查操作。因此我们的数据请求接口定义如下:
/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
文件中添加路由请求处理:
// 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) {
});
上面这部分代码搭建好了我们所定义的接口的初步框架。
接下来,我们添加数据库处理逻辑来具体实现每个接口。
首先实现 “/contacts” 请求下的 POST 接口。这个接口用来向数据库中创建和保存新的联系人。每个联系人对象将有如下的数据模型:
{
"_id": <ObjectId>
"firstName": <string>,
"lastName": <string>,
"email": <string>,
"phoneNumbers": {
"mobile": <string>,
"work": <string>
},
"twitterHandle": <string>,
"addresses": {
"home": <string>,
"work": <string>
}
}
下面的代码实现 /contacts 的 POST 请求:
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]);
}
});
});
为了测试该请求和接口,我们作如下部署:
$ git add package.json $ git add server.js $ git commit -m 'first commit' $ git push heroku master
接下来我们需要保证至少有一个应用实例正在运行:
$ heroku ps:scale web=1
随后,使用 cURL 来进行一个 POST 请求:
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
, 实现剩下所有的接口:
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();
}
});
});
完成了请求 API 之后,我们将用它来创建浏览器可访问的 Web App。
首先在项目根目录下创建一个 public
目录。随后将 example app 的 public 目录 拷贝到该目录下。 这个目录包含所有的 HTML 模板文件 和 AngularJS 代码。
在 HTML 文件中,你会发现一些非常规的 HTML 代码,比如在 index.html 中的 “ng-view”,这些其实是 AngularJS 的指令:
<div class="container" ng-view>
使用模板可以让我们复用代码并且动态地生成相应的视图。
我们将会使用 AngularJS 把客户端的所有内容整合成一个 Web App,包括管理页面路由请求、渲染不同页面的视图、向后端发送数据和接收来自后端的数据。
我们的 AngularJS 代码位于 /public/js
目录下的 app.js
文件中。我们这里重点关注主页加载时的代码,也就是 (“/”) 路由请求时需要做的事。我们需要注意以下几点:
这部分的代码如下:
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;
});
接下来,我们来关注每个部分是如何具体实现的。
路由的配置写在 routeProvider
模块。
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 service 会创建一个可以被不同请求访问的同一个对象。我们创建的服务则相当于客户端的一个容器,包含所有的 API 请求接口。
主页的路由中使用 getContacts
函数来请求联系人数据:
.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)。
到此为止,我们已经配置好了路由、定义好了需要展示的模板、利用 “Contacts” 服务取到了数据。接下来我们需要创建一个控制器 controller 来整合整个过程。
.controller("ListController", function(contacts, $scope) {
$scope.contacts = contacts.data;
})
我们的 controller 把服务端的联系人数据添加到 homepage 的 scope 作用域中,定义为变量 $scope.contacts。这样我们就可以在模板文件(比如 list.html)中直接获取这些数据。我们可以在模板中使用 AngularJS 的 ngRepeat directive对所有的contacts 数据进行迭代处理:
<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:
$ git add server.js $ git add public $ git commit -m 'second commit' $ git push heroku master
现在 web app 的所有组件都已经完成,你可以通过下面的命令打开 app 查看效果:
$ heroku open
在这篇文章中,我们重点提到了以下几点:
我们希望你可以体会到使用 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