MEAN实践——LAMP的新时代替代方案(上)

摘要:90 年代,LAMP 曾风靡一时,然而随着需求的变迁和数据流量的激增,LAMP 已不可避免的走下神坛。近日,在 MongoDB Blog 中,Dana Groce 介绍了一个基于新时代架构的实践 MEAN ,下面一块儿走进。php

**【编者按】**在九十年代,Linux+Apache+Mysql+PHP 架构曾风靡一时,直到如今仍然是众多 Web 应用程序的基本架构。然而随着需求的变迁和数据流量的激增,LAMP 已不可避免的走下神坛。近日,在 MongoDB Blog 中,Dana Groce 介绍了一个基于新时代架构的实践 —— MEAN,MongoDB/Mongoose.js、Express.js、Angular.js 和 Node.js 。html

如下为译文node

本系列博客的两篇文章主要关注 MEAN 技术堆栈的使用 —— MongoDB/Mongoose.js 、Express.js、Angular.js 和 Node.js 。这些技术都使用了 JavaScript 以获取更高的软件性能和开发者生产效率。git

第一篇博文主要描述应用程序的基本结构和进行数据建模过程,而第二篇则会建立测试来验证应用程序行为,而后介绍如何设置并运行应用程序。github

本系列博文阅读并不需求拥有这些技术的实践经验,全部技能等级的开发人员均可以从中获益。若是在这以前你没有使用过 MongoDB、JavaScript 或创建一个 REST API 的经验,不用担忧,这里将用足够的细节介绍这些主题,包括身份验证、在多文件中构建代码、编写测试用例等。首先,从 MEAN stack 的定义开始。 ##什么是 MEAN Stacksql

MEAN stack 可归纳为:mongodb

  • M = MongoDB/Mongoose.js 。流行的数据库,对 node . js 来讲是一个优雅的 ODM 。
  • E = Express.js :一个轻量级 Web 应用程序框架。
  • A = Angular.js :一个健壮的框架用于建立 HTML5 和 JavaScript-rich Web 应用程序。
  • N = Node.js 服务器端 JavaScript interpreter 。

MEAN stack 是 LAMP (Linux、Apache、MySQL,PHP / Python) stack 的一个现代替代者,在九十年代末,LAMP 曾是 Web 应用程序的主流构建方式。数据库

在这个应用程序中并不会使用 Angular.js ,由于这里并非要构建一个 HTML 用户界面。相反,这里建立的是一个没有用户界面的 REST API,但它却能够做为任何界面的基础,如一个网站、一个 Android 应用程序,或者一个 iOS 应用程序。也能够说咱们正在 ME(a)N stack 上构建 REST API ,但这不是重点! ##REST API 是什么?express

REST 表明 Representational State Transfer,是 SOAP 和 WSDL XML-based API 协议的一个更轻量级替代方案。npm

REST 使用客户端-服务器模型,服务器是一个 HTTP 服务器,而客户端发送 HTTP 行为(GET、POST、PUT、DELETE),以及 URL 编码的变量参数和一个 URL 。URL 指定了对象的做用范围,而服务器则会经过结果代码和有效的 JavaScript Object Notation (JSON) 进行响应。

由于服务器用 JSON 回复,MongoDB 与 JSON 又能够很好地交互,同时全部组件都使用了 JavaScript,所以 MEAN stack 很是适合本用例中的应用程序。在进入开始定义数据模型后,你会看到一些 JSON 的例子。

CRUD 缩略词常被用来描述数据库操做。CRUD 表明建立、读取、更新和删除。这些数据库操做能很好地映射到 HTTP 动做:

  • POST:客户想要插入或建立一个对象。
  • GET:客户端想要读取一个对象。
  • PUT:客户想要更新一个对象。
  • DELETE:客户想要删除一个对象。

在定义 API 后,这些操做将变得更加直观。REST APIs 中一般会使用的一些常见 HTTP 结果代码以下:

  • 200 ——“OK”。
  • 201 ——“Created”(和POST一块儿使用)。
  • 400 ——“Bad Request”(可能丢失所需参数)。
  • 401 ——“Unauthorized”(身份验证参数丢失)。
  • 403 ——“Forbidden”(已验证,可是权限不够)。
  • 404 ——“Not Found”。

RFC 文档中能够找到一个完整的描述,这个在本博客末尾的参考资料中列出。上面这些结果代码都会在本应用程序中使用,随后就会展现一些例子。 为何从 REST API 开始?

部署一个 REST API 能够为创建任何类型应用程序打下基础。如前文所述,这些应用程序可能会基于网络或者专门针对某些平台设计,好比 Android 或者 iOS 。

时下,已经有许多公司在创建应用程序时再也不使用 HTTP 或者 Web 接口,好比 Uber、WhatsApp、Postmates 和 Wash.io 。从一个简单的应用程序发展成一个强大的平台,REST API 能够大幅度简化这个过程当中其余接口和应用程序的实现。 ##创建 REST API

这里会创建一个 RSS Aggregator,相似Google Reader,应用程序主要会包含两个组件:

  1. REST API
  2. Feed Grabber(相似 Google Reader)

本系列博文都将聚焦这个 REST API 的打造,不会去关注 RSS feeds 的复杂性。如今,Feed Grabber 的代码已经能够在 github repository 中发现,详情能够见博文列出的资源。下面将介绍打造这个 API 所需的步骤。首先会根据具体需求来定义数据模型:

  • 在用户帐户中储存用户信息
  • 跟踪须要被监视的RSS feeds
  • 将feed记录pull到数据库
  • 跟踪用户feed订阅
  • 跟踪用户会阅读哪一个订阅的feed

用户则须要能够完成下列操做:

  • 创建一个帐户
  • 到feed的订阅或者退订
  • 阅读feed记录
  • 标记feed/记录的阅读状态(已读/未读)

##数据建模

这里不会深刻讨论 MongoDB 中的数据建模,详细资料能够在博文后的列举的资料中发现。本用例须要 4 个 collections 来管理这个信息:

  • Feed collection
  • Feed entry collection
  • User collection
  • User-feed-entry mapping collection

##Feed Collection

下面一块儿进入一段代码,Feed Collection 的建模能够经过下述 JSON 文档完成:

{
"_id": ObjectId("523b1153a2aa6a3233a913f8"),
"requiresAuthentication": false,
"modifiedDate": ISODate("2014-08-29T17:40:22Z"),
"permanentlyRemoved": false,
"feedURL": "http://feeds.feedburner.com/eater/nyc",
"title": "Eater NY",
"bozoBitSet": false,
"enabled": true,
"etag": "4bL78iLSZud2iXd/vd10mYC32BE",
"link": "http://ny.eater.com/",
"permanentRedirectURL": null,
"description": "The New York City Restaurant, Bar, and Nightlife Blog”
}

若是你精通关系型数据库技术,那么你将了解数据库、表格、列和行。在 MongoDB 中,大部分的关系型概念均可以映射。从高等级看,MongoDB 部署支持 1 个或者多个数据库。1 个数据库可能包含多个 collection,这个相似于传统关系型数据库中的表格。Collection 中会有多个 document,从高等级看,document 至关于关系型数据库中的行。这里须要注意的是,MongoDB 中的 document 并无预设的格式,取而代之,每一个 document 中均可以有 1 个或者多个的键值对,这里的值多是简单的,好比日期,也能够是复杂的,好比 1 个地址对象数组。

上文的 JSON 文档是一个 Eater Blog 的 RSS feed 示例,它会跟踪纽约全部餐馆信息。所以,这里可能存在许多字段,而用例中主要关注的则是 feed 中的 URL 以及 description 。描述是很是重要的,所以在创建一个移动应用程序时,它会是 feed 一个很好的摘要。

JSON 中的其余字段用于内部使用,其中很是重要的字段是 _id 。在 MongoDB 中,每一个 document 都须要拥有一个 _id 字段。若是你创建一个没有 —— id 的 document,MongoDB 将为你自动添加。在 MongoDB 中,这个字段就是主键的存在,所以 MongoDB 会保证这个字段值在 collection 范围惟一。 ##Feed Entry Collection

在 feed 以后,用例中还指望追踪 feed 记录。下面是一个 Feed Entry Collection 文档示例:

{
    "_id": ObjectId("523b1153a2aa6a3233a91412"),
    "description": "Buzzfeed asked a bunch of people...”,
    "title": "Cronut Mania: Buzzfeed asked a bunch of people...",
    "summary": "Buzzfeed asked a bunch of people that were...”,
    "content": [{
        "base": "http://ny.eater.com/",
        "type": "text/html",
        "value": ”LOTS OF HTML HERE ",
        "language": "en"
    }],
    "entryID": "tag:ny.eater.com,2013://4.560508",
    "publishedDate": ISODate("2013-09-17T20:45:20Z"),
    "link": "http://ny.eater.com/archives/2013/09/cronut_mania_41    .php",
    "feedID": ObjectId("523b1153a2aa6a3233a913f8")
}

再次提醒,这里一样必须拥有一个 _id 字段,同时也能够看到 description、title 和 summary 字段。对于 content 字段,这里使用的是数组,数据中一样储存了一个 document。MongoDB 容许经过这种方式嵌套使用 document,同时这个用法在许多场景中也是很是必要的,由于用例每每需求将信息集中存储。

entryID 字段使用了 tag 格式来避免复制 feed 记录。这里须要注意的是 feedID 和 ObjectId 的用法——值则是 Eater Blog document 的 _id 。这提供了一个参考模型,相似关系型数据库中的外键。所以,若是指望查看这个 ObjectId 关联的 feed document,能够取值 523b1153a2aa6a3233a913f8,并在 _id 上查询 feed collection,从而就会返回 Eater Blog document。 ##User Collection

这里有一个用户须要使用的 document :

{
    "_id" : ObjectId("54ad6c3ae764de42070b27b1"),
    "active" : true,
    "email" : "testuser1@example.com",
    "firstName" : "Test",
    "lastName" : "User1",
    "sp_api_key_id" : "6YQB0A8VXM0X8RVDPPLRHBI7J",
    "sp_api_key_secret" : "veBw/YFx56Dl0bbiVEpvbjF”,
    "lastLogin" : ISODate("2015-01-07T17:26:18.996Z"),
    "created" : ISODate("2015-01-07T17:26:18.995Z"),
    "subs" : [ ObjectId("523b1153a2aa6a3233a913f8"),
                                ObjectId("54b563c3a50a190b50f4d63b") ],
}

用户应该有 email 地址、first name 和 last name。一样,这里还存在 sp_api_key_id 和 sp_api_key_secret —— 在后续部分会结合 Stormpath(一个用户管理 API )使用这两个字段。最后一个字段 subs,是 1 个订阅数组。subs 字段会标明这个用户订阅了哪些 feeds。 ##User-Feed-Entry Mapping Collection

{
 	"_id" : ObjectId("523b2fcc054b1b8c579bdb82"),
    "read" : true,
    "user_id" : ObjectId("54ad6c3ae764de42070b27b1"),
    "feed_entry_id" : ObjectId("523b1153a2aa6a3233a91412"),
    "feed_id" : ObjectId("523b1153a2aa6a3233a913f8")
}

最后一个 collection 容许映射用户到 feeds,并跟踪哪些 feeds 已经读取。在这里,使用一个布尔类型(true/false)来标记已读和未读。 ##REST API 的一些功能需求

如上文所述,用户须要能够完成如下操做:

  • 创建一个帐户
  • 到 feed 的订阅或者退订
  • 阅读 feed 记录
  • 标记 feed / 记录的阅读状态(已读 / 未读)

此外,用户还需求能够重置密码。下表表示了这些操做是如何映射到 HTTP 路由和动做。

在生产环境中,HTTP(HTTPS)安全需求使用一个标准的途径来发送敏感信息,好比密码。 ##经过 Stormpath 实现现实世界中的身份验证

在一个鲁棒的现实世界应用程序中,提供用户身份验证不可避免。所以,这里须要一个安全的途径来管理用户、密码和密码重置。

在本用例中,可使用多种方式进行身份验证。其中一个就是使用 Node.js 搭配 Passport Plugin ,这个方式一般被用于社交媒体帐户验证中,好比 Facebook 或者 Twitter 。然而,Stormpath 一样是一个很是不错的途径。Stormpath 是一个用户管理即服务,支持身份验证和经过 API keys 受权。根本上,Stormpath 维护了一个用户详情和密码数据库,从而客户端应用程序 API 能够调用 Stormpath REST API 来进行用户身份验证。

下图显示了使用 Stormpath 后的请求和响应流。

详细来讲,Stormpath 会为每一个应用程序提供一个安全秘钥,经过它们的服务来定义。举个例子,这里能够定义一个应用程序做为「Reader Production」或者「Reader Test」。若是一直对应用程序进行开发和测试,定义这两个应用程序很是实用,由于增长和删除测试用户会很是频繁。在这里,Stormpath 一样会提供一个 API Key Properties 文件。Stormpath 一样容许基于应用程序的需求来定义密码属性,好比:

  • 不低于 8 个字符
  • 必须包含大小写
  • 必须包含数字
  • 必须包含 1 个非字母字符

Stormpath 会跟踪全部用户,并分配他们的 API keys(用于 REST API 身份验证),这将大幅度简化应用程序创建过程,由于这里再也不须要为验证用户编写代码。 ##Node.js

Node.js 是服务器端和网络应用程序的运行时环境。Node.js 使用 JavaScript 并适合多种不一样的平台,好比 Linux、Microsoft Windows 和 Apple OS X。

Node.js 应用程序须要经过多个库模块创建,当下社区中已经有了很是多的资源,后续应用程序创建中也会使用到。

为了使用 Node.js,开发者须要定义 package.json 文件来描述应用程序以及全部库的依赖性。

Node.js Package Manager 会安装全部库的副本到应用程序目录的一个子目录,也就是 node_modules/ 。这么作有必定的好处,由于这样作能够隔离不一样应用程序的库版本,同时也避免了全部库都被统一安装到标准目录下形成的代码复杂性,好比 /usr/lib。

命令 npm 会创建 node_modules/ 目录,以及全部须要的库。

下面是 package.json 文件下的 JavaScript:

{
    "name": "reader-api",
    "main": "server.js",
    "dependencies": {
	"express" : "~4.10.0",
    "stormpath" : "~0.7.5", "express-stormpath" : "~0.5.9",
    "mongodb" : "~1.4.26”, "mongoose" : "~3.8.0",
    "body-parser" : "~1.10.0”, "method-override" : "~2.3.0",
    "morgan" : "~1.5.0”, "winston" : "~0.8.3”, "express-winston" : "~0.2.9",
	"validator" : "~3.27.0",
    "path" : "~0.4.9",
    "errorhandler" : "~1.3.0",
    "frisby" : "~0.8.3",
    "jasmine-node" : "~1.14.5",
    "async" : "~0.9.0"
    }
}

应用程序被命名为 reader-api,主文件被命名为 server.js,随后会是一系列的依赖库和它们的版本。这些库其中的一些被设计用来解析 HTTP 查询。在这里,咱们会使用 frisby 做为测试工具,而 jasmine-node 则被用来运行 frisby 脚本。

在这些库中,async 尤其重要。若是你从未使用过 node.js,那么请注意 node.js 使用的是异步机制。所以,任何阻塞 input/output (I/O) 的操做(好比从 socket 中读取或者 1 个数据库查询)都会采用一个回调函数做为最后的参数,而后继续控制流,只有在阻塞操做结束后才会继续这个回调函数。下面看一个简单的例子来理解这一点。

function foo() { someAsyncFunction(params, function(err, results)     { console.log(“one”);
    }); console.log(“two”); }

在上面这个例子中,你想象中的输出多是:

one
two

但实际状况的输出是:

two
one

形成这个结果的缘由就是 Node.js 使用的异步机制,打印 「one」 的代码可能会在后续的回调函数中执行。之因此说可能,是由于这只在必定的情景下发生。这种异步编程带来的不肯定性被称之为 non-deterministic execution 。对于许多编程任务来讲,这么作能够得到很高的性能,可是在顺序性要求的场景则很是麻烦。而经过下面的用法则能够得到一个理想中的顺序:

actionArray = [ function one(cb) { someAsyncFunction(params, function(err,
        results) { if (err) { cb(new Error(“There was an  error”)); } console.log(“one”);
        cb(null); }); }, function two(cb) { console.log(“two”); cb(null); } ] async.series(actionArray);

##总结

经过本篇文章,相信你们对 Node.js 和异步函数设置都有了必定的理解,所以下篇博文将会描述更深刻层次的一些知识。取代开始创建应用程序,这里会进入创建测试以及验证应用程序的行为。这种方式则被称为 test-driven 开发,它会带来两大好处:

首先,它会帮助开发者弄清数据和函数的消费方式,同时也能够帮助弄清一些奇怪的需求,好比数组中会储存多个对象。

经过在创建应用程序以前编写测试,模型会从「assumed to be working until a test fails」转换成「broken / unimplemented until proven tested OK」。对于创建一个更健壮的应用程序来讲,前者显然更安全些。

未完待续。

原文连接Building your first application with MongoDB: Creating a REST API using the MEAN Stack - Part 1

本文系 OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客

相关文章
相关标签/搜索