在众成翻译上看到了这篇文章 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 的实际运行效果:
现在,我们开始一步一步来
创建一个新项目目录,随后 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 实例来保存应用数据。我们将会使用一个部署在云端的数据库服务平台 mLab 来创建 MongoDB 数据库。
想要把 mLab 插件添加到应用中,需要在 Heroku CLI 上运行以下命令:
Bash $ 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 数据库,内容如下:
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 ());
var db;
mongodb.MongoClient .connect (process.env .MONGODB_URI , function (err, database ) {
if (err) {
console .log (err);
process.exit (1 );
}
db = database;
console .log ("Database connection ready" );
var server = app.listen (process.env .PORT || 8080 , function ( ) {
var port = server.address ().port ;
console .log ("App now running on port" , port);
});
});
连接数据库的时候,有以下几点需要注意:
尽可能多地使用数据库连接池 connection pool 来管理应用资源. 我们在全局作用域中初始化 db
变量,这样所有的路由控制器都能访问到数据库连接。 在数据库连接完成之后再初始化应用,这样能够保证应用在进行数据库操作时不会崩溃或抛出错误。 至此,app 和数据库已经连接了,接下来我们会来实现 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
function handleError (res, reason, message, code ) {
console .log ("ERROR: " + reason);
res.status (code || 500 ).json ({"error" : message});
}
app.get ("/contacts" , function (req, res ) {
});
app.post ("/contacts" , function (req, res ) {
});
app.get ("/contacts/:id" , function (req, res ) {
});
app.put ("/contacts/:id" , function (req, res ) {
});
app.delete ("/contacts/:id" , function (req, res ) {
});
上面这部分代码搭建好了我们所定义的接口的初步框架。
接下来,我们添加数据库处理逻辑来具体实现每个接口。
首先实现 “/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 ());
var db;
mongodb.MongoClient .connect (process.env .MONGODB_URI , function (err, database ) {
if (err) {
console .log (err);
process.exit (1 );
}
db = database;
console .log ("Database connection ready" );
var server = app.listen (process.env .PORT || 8080 , function ( ) {
var port = server.address ().port ;
console .log ("App now running on port" , port);
});
});
function handleError (res, reason, message, code ) {
console .log ("ERROR: " + reason);
res.status (code || 500 ).json ({"error" : message});
}
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 ]);
}
});
});
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 的指令:
HTML, XML <div class ="container" ng-view >
使用模板可以让我们复用代码并且动态地生成相应的视图。
我们将会使用 AngularJS 把客户端的所有内容整合成一个 Web App,包括管理页面路由请求、渲染不同页面的视图、向后端发送数据和接收来自后端的数据。
我们的 AngularJS 代码位于 /public/js
目录下的 app.js
文件中。我们这里重点关注主页加载时的代码,也就是 (“/”) 路由请求时需要做的事。我们需要注意以下几点:
这部分的代码如下:
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 ;
});
接下来,我们来关注每个部分是如何具体实现的。
路由的配置写在 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 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)。
到此为止,我们已经配置好了路由、定义好了需要展示的模板、利用 “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