使用 MEAN Stack 创建联系人管理器应用程序
使用 MEAN Stack 创建联系人管理器应用程序
引言
MEAN堆栈是一项可用于开发Web应用程序的技术,MEAN是由MongoDB、ExpressJS、AngularJS和NodeJS组成的堆栈。借助MEAN堆栈,我们可以获得开发完整应用程序的所有组件。
- MongoDB:数据库
- ExpressJS:RESTful API
- AngularJS:演示
- NodeJS:JavaScript服务器端
技能前提
- JavaScript
- 数据库概念
- 命令行
软件先决条件
- NodeJS
- MongoDB
- 文本编辑器(VS Code、Vim等)
Using the Code
步骤01 - 环境设置
为了能够使用本指南,我们需要安装NodeJS,因此请访问此链接下载最新版本的NodeJS。下载完成后,运行安装程序,按照步骤操作,安装完成后,打开命令行终端并测试以下命令:node -v
,该命令显示NodeJS版本,在我这里显示的是v5.9.1。
接下来安装MongoDB,从MongoDB下载中心下载安装程序,安装并按照安装向导中的步骤进行操作。在我这里,安装目录是:C:\Program Files\MongoDB\Server\3.2\bin,我们可以使用mongod命令启动MongoDB服务,您将看到类似如下的输出:
关于ExpressJS和AngularJS,我们可以通过NPM从命令行安装它们,NPM是Node包管理器,我们也可以卸载包。例如,如果我们想安装这两个包,我们可以在命令行中输入以下命令:
npm install angular
npm install express
如果我们输入npm install
,NPM将使用package.json文件来知道要安装哪些包。如果您不知道如何编写package.json的内容,只需在命令行中输入npm init
并提供项目信息即可。
步骤02 - 添加代码
环境设置完成后,我们将使用MEAN堆栈创建一个联系人管理器应用程序,我们继续打开命令行并按照以下步骤操作:
- 创建一个名为ContactManager.Mean的目录
- 切换到新目录
- 执行此命令:npm init并为您的项目提供信息
- 在package.json文件中添加
body-parse
包 - 在package.json文件中添加
express
包 - 在package.json文件中添加
mongojs
包 - 保存更改并恢复包
我们的package.json文件如下:
{
"name": "contactmanager",
"version": "1.0.0",
"description": "Contact Manager App developed with MEAN Stack",
"main": "server.js",
"dependencies": {
"body-parser": "^1.10.2",
"express": "^4.11.1",
"mongojs": "^0.18.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/hherzl/ContactManager.Mean.git"
},
"author": "C. Herzl",
"license": "ISC",
"bugs": {
"url": "https://github.com/hherzl/ContactManager.Mean/issues"
},
"homepage": "https://github.com/hherzl/ContactManager.Mean#readme"
}
现在,我们将继续创建一个名为Server.js的文件,其中包含以下代码:
var express = require("express");
var bodyParser = require("body-parser");
var app = express();
app.use(express.static(__dirname + "/public"));
app.use(bodyParser.json());
var rest = require("./api");
var api = new rest(app);
var port = 3000;
app.listen(port);
console.log("Server running on port " + port);
api.js代码文件
function api (app) {
var mongojs = require("mongojs");
var db = mongojs("contactManager", ["contacts"]);
app.get("/api/contact", function (request, response) {
var pageSize = request.query.pageSize ? parseInt(request.query.pageSize) : 10;
var firstName = request.query.firstName;
var middleName = request.query.middleName;
var lastName = request.query.lastName;
var find = {};
if (firstName) {
find.firstName = new RegExp(firstName, "i");
}
if (middleName) {
find.middleName = new RegExp(middleName, "i");
}
if (lastName) {
find.lastName = new RegExp(lastName, "i");
}
var fields = {
firstName: 1,
middleName: 1,
lastName: 1,
phone: 1,
email: 1
};
var result = db.contacts.find(find, fields).limit
(pageSize, function (err, docs) {
response.json(docs);
});
});
app.get("/api/contact/:id", function (request, response) {
var id = request.params.id;
db.contacts.findOne({ _id: mongojs.ObjectId(id) }, function (err, doc) {
if (err)
console.log("Error: " + err);
response.json(doc);
});
});
app.post("/api/contact", function (request, response) {
db.contacts.insert(request.body, function (err, doc) {
if (err)
console.log("Error: " + err);
response.json(doc);
});
});
app.put("/api/contact/:id", function (request, response) {
var id = request.params.id;
db.contacts.findAndModify({
query: {
_id: mongojs.ObjectId(id)
},
update: {
$set: {
firstName: request.body.firstName,
middleName: request.body.middleName,
lastName: request.body.lastName,
phone: request.body.phone,
email: request.body.email
}
},
new: true
}, function (err, doc) {
response.json(doc);
});
});
app.delete("/api/contact/:id", function (request, response) {
var id = request.params.id;
db.contacts.remove({ _id: mongojs.ObjectId(id) }, function (err, doc) {
if (err)
console.log("Error: " + err);
response.json(doc);
});
});
};
module.exports = api;
在server.js文件的开头,我们使用require
函数加载所有依赖项,然后我们使用app.use(express.static(__dirname + "/public"));
在公共目录中启用静态内容,我们还通过以下行来设置所有请求的body解析器:app.use(bodyParser.json());
接下来,我们添加所有API操作,最后,我们设置应用程序的监听端口:app.listen(3000);
正如我们所看到的,在这个文件中,我们使用ExpressJS框架来定义API操作,目前我们有以下URL:
动词 | URL | 描述 |
GET | api/contact | 检索所有联系人 |
GET | api/contact/id | 按id检索联系人 |
POST | api/contact | 创建一个新联系人 |
PUT | api/contact | 更新现有联系人 |
删除 | api/contact | 删除现有联系人 |
所有这些URL都用于API,在server.js内部,我们有访问MongoDB数据库的实例,名为“contacts”的“表”必须具有以下列:
firstName
middleName
lastName
phone
email
我们可以根据需要设置API操作的约定。就我而言,我更喜欢使用api前缀,并使用单数名称来表示API中的实体或功能。
关于AngularJS,我们继续创建以下目录:
- controllers:控制器存放位置
- services:服务存放位置(控制器可重用对象)
- views:HTML文件存放位置
现在,我们在根目录中添加一个名为app.js的文件来定义AngularJS模块。
var module = angular.module("contactManager", [
"ngRoute",
"ngAnimate",
"toaster"
]);
(function (app) {
app.config(function ($routeProvider) {
var base = "/views/";
$routeProvider
.when("/", {
templateUrl: base + "contact/index.html",
controller: "HomeController",
controllerAs: "vm"
})
.when("/contact/add", {
templateUrl: base + "contact/add.html",
controller: "ContactAddController",
controllerAs: "vm"
})
.when("/contact/details/:id", {
templateUrl: base + "contact/details.html",
controller: "ContactDetailsController",
controllerAs: "vm"
})
.when("/contact/edit/:id", {
templateUrl: base + "contact/edit.html",
controller: "ContactEditController",
controllerAs: "vm"
})
.when("/contact/remove/:id", {
templateUrl: base + "contact/remove.html",
controller: "ContactRemoveController",
controllerAs: "vm"
});
});
})(angular.module("contactManager"));
此文件包含AngularJS模块和路由表的定义,我们对路由表使用相同的命名约定,对实体使用单数。
在services目录中,我们将放置模块的所有可注入服务。在此情况下,在此服务中,我们将添加提供所有API相关操作的代码。
(function (app) {
"use strict";
app.service("RepositoryService", RepositoryService);
RepositoryService.$inject = ["$log", "$http"];
function RepositoryService($log, $http) {
var svc = this;
var apiUrl = "/api";
svc.getContacts = getContacts;
svc.getContact = getContact;
svc.createContact = createContact;
svc.updateContact = updateContact;
svc.deleteContact = deleteContact;
function getContacts(fields) {
var queryString = [];
if (fields.pageSize) {
queryString.push("pageSize=" + fields.pageSize);
}
if (fields.firstName) {
queryString.push("firstName=" + fields.firstName);
}
if (fields.middleName) {
queryString.push("middleName=" + fields.middleName);
}
if (fields.lastName) {
queryString.push("lastName=" + fields.lastName);
}
var url = [apiUrl, "contact"].join("/");
var fullUrl = queryString.length == 0 ?
url : [url, "?", queryString.join("&")].join("");
return $http.get(fullUrl);
};
function getContact(id) {
return $http.get([apiUrl, "contact", id].join("/"));
};
function createContact(model) {
return $http.post([apiUrl, "contact"].join("/"), model);
};
function updateContact(id, model) {
return $http.put([apiUrl, "contact", id].join("/"), model);
};
function deleteContact(id) {
return $http.delete([apiUrl, "contact", id].join("/"));
};
};
})(angular.module("contactManager"));
这是一个好习惯,因为使用服务可以避免在所有控制器中注入$http
服务。相反,我们将注入RepositoryService
,因为将来如果我们更改API的URL,我们不必担心更改所有控制器,我们只需更改服务。
在controllers目录中,我们将为联系人的每个操作创建一个控制器:
HomeController
ContactAddController
ContactDetailsController
ContactEditController
ContactRemoveController
Home
(function (app) {
"use strict";
app.controller("HomeController", HomeController);
HomeController.$inject = ["$location", "toaster", "RepositoryService"];
function HomeController($location, toaster, repository) {
var vm = this;
vm.contacts = [];
vm.search = {};
vm.add = add;
vm.search = search;
vm.details = details;
vm.remove = remove;
toaster.pop("wait", "Loading contacts...");
repository.getContacts(vm.search).then(function (result) {
vm.contacts = result.data;
});
vm.add = function () {
$location.path("/contact/add/");
};
vm.search = function () {
repository.getContacts(vm.search).then(function (result) {
vm.contacts = result.data;
});
};
vm.details = function (id) {
$location.path("/contact/details/" + id);
};
vm.remove = function (id) {
$location.path("/contact/remove/" + id);
};
};
})(angular.module("contactManager"));
index.html代码文件
<h2>Contacts</h2>
<p>
<a ng-click="vm.add()">Add new</a>
</p>
<div class="container">
<div class="row">
<div class="form-inline">
<div class="form-group">
<input type="text" name="firstName" class="form-control"
placeholder="First name" ng-model="vm.search.firstName" />
</div>
<div class="form-group">
<input type="text" name="middleName" class="form-control"
placeholder="Middle name" ng-model="vm.search.middleName" />
</div>
<div class="form-group">
<input type="text" name="lastName" class="form-control"
placeholder="Last name" ng-model="vm.search.lastName" />
</div>
<div class="form-group">
<select name="pageSize" class="form-control list-box"
ng-model="vm.search.pageSize">
<option value="10" selected="selected">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="form-group">
<button type="button"
class="btn btn-primary glyphicon glyphicon-search"
ng-click="vm.search();" />
</div>
</div>
</div>
</div>
<br />
<table class="table table-hover">
<tr>
<th>First name</th>
<th>Middle name</th>
<th>Last name</th>
<th>Phone</th>
<th>Email</th>
<th></th>
</tr>
<tr ng-repeat="contact in vm.contacts">
<td>{{ contact.firstName }}</td>
<td>{{ contact.middleName }}</td>
<td>{{ contact.lastName }}</td>
<td>{{ contact.phone }}</td>
<td>{{ contact.email }}</td>
<td>
<div class="dropdown">
<button class="btn btn-primary btn-lg dropdown-toggle"
type="button" data-toggle="dropdown">
<span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a ng-click="vm.details(contact._id)">Details</a></li>
<li><a ng-click="vm.remove(contact._id)">Remove</a></li>
</ul>
</div>
</td>
</tr>
</table>
我们定义了控制器并注入了所有必需的服务:$location
和RepositoryService
,第一个服务用于重定向,第二个服务是API存储库。
请注意,我们没有使用$scope
服务,而是使用了控制器的别名:vm
,事实上,在所有HTML文件中,我们都将始终使用vm
别名来访问控制器的成员。
添加联系人
(function (app) {
"use strict";
app.controller("ContactAddController", ContactAddController);
ContactAddController.$inject = ["$location", "toaster", "RepositoryService"];
function ContactAddController($location, toaster, repository) {
var vm = this;
vm.model = {};
vm.save = save;
vm.cancel = cancel;
function save() {
toaster.pop("wait", "Saving...");
repository.createContact(vm.model).then(function (result) {
toaster.pop("success", "The contact was saved successfully");
$location.path("/");
});
};
function cancel() {
$location.path("/");
};
};
})(angular.module("contactManager"));
add.html代码文件
<h2>Create</h2>
<form name="form" novalidate ng-submit="vm.save()">
<div class="form-horizontal">
<h4>Contact</h4>
<hr />
<div class="form-group" ng-class="{ 'has-error': form.firstName.$invalid }">
<label for="firstName" class="control-label col-md-2">First name</label>
<div class="col-md-10">
<input type="text" name="firstName" class="form-control"
required ng-model="vm.model.firstName" />
<span class="text-danger help-block"
ng-show="form.firstName.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group">
<label for="middleName" class="control-label col-md-2">Middle name</label>
<div class="col-md-10">
<input type="text" name="middleName" class="form-control"
ng-model="vm.model.middleName" />
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': form.lastName.$invalid }">
<label for="lastName" class="control-label col-md-2">Last name</label>
<div class="col-md-10">
<input type="text" name="lastName" class="form-control"
required ng-model="vm.model.lastName" />
<span class="text-danger help-block"
ng-show="form.lastName.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': form.phone.$invalid }">
<label for="phone" class="control-label col-md-2">Phone</label>
<div class="col-md-10">
<input type="text" name="phone" class="form-control"
required ng-model="vm.model.phone" />
<span class="text-danger help-block"
ng-show="form.phone.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': form.email.$invalid }">
<label for="email" class="control-label col-md-2">Email</label>
<div class="col-md-10">
<input type="text" name="email" class="form-control"
required ng-model="vm.model.email" />
<span class="text-danger help-block"
ng-show="form.email.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" ng-disabled="!form.$valid"
class="btn btn-primary" />
<a ng-click="vm.cancel()">Cancel</a>
</div>
</div>
</div>
</form>
联系人详细信息
(function (app) {
"use strict";
app.controller("ContactDetailsController", ContactDetailsController);
ContactDetailsController.$inject =
["$routeParams", "$location", "RepositoryService"];
function ContactDetailsController($routeParams, $location, repository) {
var vm = this;
var id = $routeParams.id;
vm.model = {};
vm.edit = edit;
vm.cancel = cancel;
repository.getContact(id).then(function (result) {
vm.model = result.data;
});
function edit() {
$location.path("/contact/edit/" + id);
};
function cancel() {
$location.path("/");
};
};
})(angular.module("contactManager"));
details.html代码文件
<h2>Details</h2>
<div>
<h4>Contact</h4>
<hr />
<dl class="dl-horizontal">
<dt>First name</dt>
<dd>{{ vm.model.firstName }}</dd>
<dt>Middle name</dt>
<dd>{{ vm.model.middleName }}</dd>
<dt>Last name</dt>
<dd>{{ vm.model.lastName }}</dd>
<dt>Phone</dt>
<dd>{{ vm.model.phone }}</dd>
<dt>Email</dt>
<dd>{{ vm.model.email }}</dd>
</dl>
</div>
<p>
<a ng-click="vm.edit()">Edit</a> | <a ng-click="vm.cancel()">Cancel</a>
</p>
编辑联系人
(function (app) {
"use strict";
app.controller("ContactEditController", ContactEditController);
ContactEditController.$inject =
["$routeParams", "$location", "toaster", "RepositoryService"];
function ContactEditController($routeParams, $location, toaster, repository) {
var vm = this;
var id = $routeParams.id;
vm.model = {};
vm.save = save;
vm.cancel = cancel;
repository.getContact(id).then(function (result) {
vm.model = result.data;
});
function save() {
toaster.pop("wait", "Saving...");
repository.updateContact(id, vm.model).then(function (result) {
toaster.pop("success", "The changes were saved successfully");
$location.path("/contact/details/" + id);
});
};
function cancel() {
$location.path("/contact/details/" + id);
};
};
})(angular.module("contactManager"));
edit.html代码文件
<h2>Edit</h2>
<form name="form" novalidate ng-submit="vm.save()">
<div class="form-horizontal">
<h4>Contact</h4>
<hr />
<div class="form-group" ng-class="{ 'has-error': form.firstName.$invalid }">
<label for="firstName" class="control-label col-md-2">First name</label>
<div class="col-md-10">
<input type="text" name="firstName"
class="form-control" required ng-model="vm.model.firstName" />
<span class="text-danger help-block"
ng-show="form.firstName.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group">
<label for="middleName" class="control-label col-md-2">Middle name</label>
<div class="col-md-10">
<input type="text" name="middleName"
class="form-control" ng-model="vm.model.middleName" />
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': form.lastName.$invalid }">
<label for="lastName" class="control-label col-md-2">Last name</label>
<div class="col-md-10">
<input type="text" name="lastName"
class="form-control" required ng-model="vm.model.lastName" />
<span class="text-danger help-block"
ng-show="form.lastName.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': form.phone.$invalid }">
<label for="phone" class="control-label col-md-2">Phone</label>
<div class="col-md-10">
<input type="text" name="phone"
class="form-control" required ng-model="vm.model.phone" />
<span class="text-danger help-block"
ng-show="form.phone.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': form.email.$invalid }">
<label for="email" class="control-label col-md-2">Email</label>
<div class="col-md-10">
<input type="text" name="email" class="form-control"
required ng-model="vm.model.email" />
<span class="text-danger help-block"
ng-show="form.email.$error.required">(*) Required</span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save"
ng-disabled="!form.$valid" class="btn btn-primary" />
<a ng-click="vm.cancel()">Cancel</a>
</div>
</div>
</div>
</form>
删除联系人
(function (app) {
"use strict";
app.controller("ContactRemoveController", ContactRemoveController);
ContactRemoveController.$inject =
["$location", "$routeParams", "toaster", "RepositoryService"];
function ContactRemoveController($location, $routeParams, toaster, repository) {
var vm = this;
var id = $routeParams.id;
vm.model = {};
vm.remove = remove;
vm.cancel = cancel;
repository.getContact(id).then(function (result) {
vm.model = result.data;
});
function remove() {
toaster.pop("wait", "Removing...");
repository.deleteContact(id).then(function (result) {
toaster.pop("success", "The contact was removed successfully");
$location.path("/");
});
};
function cancel() {
$location.path("/");
};
};
})(angular.module("contactManager"));
remove.html代码文件
<h2>Remove</h2>
<h3>Are you sure to want remove this record?</h3>
<div>
<h4>Contact</h4>
<hr />
<dl class="dl-horizontal">
<dt>First name</dt>
<dd>{{ vm.model.firstName }}</dd>
<dt>Middle name</dt>
<dd>{{ vm.model.middleName }}</dd>
<dt>Last name</dt>
<dd>{{ vm.model.lastName }}</dd>
<dt>Phone</dt>
<dd>{{ vm.model.phone }}</dd>
<dt>Email</dt>
<dd>{{ vm.model.email }}</dd>
</dl>
<div class="form-actions no-color">
<a ng-click="vm.remove(vm.model._id)" class="btn btn-default">Delete</a>
<a ng-click="vm.cancel()" class="btn btn-small">Cancel</a>
</div>
</div>
最后,我们将拥有以下结构:
现在,在命令行中,执行此命令:node server.js
,然后打开浏览器并加载URL:https://:3000/,尝试在应用程序中添加、编辑和删除联系人,所有ID都由MongoDB引擎生成,因此我们无需担心ID。
步骤03 - 添加联系人模拟器
contact.mocker.js代码文件
var mongojs = require("mongojs");
var db = mongojs("contactManager", ["contacts"]);
var limit = 1000;
var firstNames = ["James", "John", "Robert", "Michael", "William",
"David", "Richard", "Charles", "Joseph", "Thomas"];
var middleNames = ["Peter", "Lee", "Alexander", "Daniel", "Edward"];
var lastNames = ["Smith", "Johnson", "Williams", "Jones", "Brown",
"Davis", "Miller", "Wilson", "Moore"];
var getRandomValue = function (min, max) {
return Math.floor(Math.random() * ((max - min) + 1)) + min;
};
var getRandomItem = function (array) {
return array[Math.floor(Math.random() * array.length)];
};
var phoneMocker = function () {
var areaCode = getRandomValue(250, 500);
var phoneNumber = getRandomValue(2000000, 5000000);
return [areaCode, " ", phoneNumber].join("");
};
var emailMocker = function (contact) {
var separators = ["", ".", "_", "", ".", "_"];
var separator = getRandomItem(separators);
var contactInfo = [contact.firstName, separator,
contact.middleName, separator, contact.lastName].join("");
var domains = ["outlook.com", "gmail.com", "yahoo.com"];
return contactInfo.toLowerCase() + "@" + getRandomItem(domains);
};
for (var i = 0; i < limit; i++) {
var firstName = getRandomItem(firstNames);
var middleName = getRandomItem(middleNames);
var lastName = getRandomItem(lastNames);
var item = {
firstName: firstName,
middleName: middleName,
lastName: lastName,
phone: phoneMocker()
};
item.email = emailMocker(item);
console.log("Inserting new row...");
db.contacts.insert(item, function (err, doc) {
if (err) {
console.log(err);
}
});
}
要生成集合中的数据,我们可以从命令行运行contact.mocker.js文件:node contact.mocker.js。我们还可以设置记录的数量,在此示例中,限制为1000
;名字、中间名和姓氏来自统计数据。
工作原理?它通过一个for
循环来创建所有联系人,对于每个联系人,它从数组中以随机方式获取名字、中间名和姓氏,电话号码在一定范围内,电子邮件根据名字和随机分隔符生成。
代码改进
- 为字段添加最大长度和正则表达式验证
- 为AngularJS控制器添加单元测试
- 添加身份验证
关注点
- JavaScript是一种非类型化语言,但我们想用强类型语言开发,我们可以使用TypeScript,TypeScript是JavaScript的超集。
- 对于JavaScript,我们使用严格模式,根据最佳实践,这使得编写“安全”的JavaScript更容易。
- 如果我们需要访问另一种类型的数据库,我们可以安装所需的包并更改访问数据库的逻辑。
相关链接
历史
- 2017年2月5日:初始版本
- 2017年2月6日:添加了环境设置说明
- 2017年2月22日:添加了联系人模拟器
- 2017年10月3日:函数定义更改