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 官方博客

相關文章
相關標籤/搜索