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 的定义开始。sql

什么是 MEAN Stack

MEAN stack 可归纳为:mongodb

  • M = MongoDB/Mongoose.js 。流行的数据库,对 node . js 来讲是一个优雅的 ODM 。数据库

  • E = Express.js :一个轻量级 Web 应用程序框架。express

  • A = Angular.js :一个健壮的框架用于建立 HTML5 和 JavaScript-rich Web 应用程序。npm

  • 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 是什么?

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

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。一样,这里还存在 spapikeyid 和 spapikeysecret —— 在后续部分会结合 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 官方博客

相关文章
相关标签/搜索