學習Node全棧開發,你都需要掌握哪些知識?

本書絕大部分內容關註的都是組成Node的核心模塊和功能。我之所以盡量避免使用第三方模塊,是因為Node還是一個很不穩定的環境,所以它對第三方模塊的支持會隨著時間的推移而發生快速且劇烈的變化。

但是我覺得,如果不瞭解更廣泛的Node應用上下文的話,我們就無法真正掌握Node。換句話說,你需要熟悉Node全棧開發。這意味著你需要熟悉數據系統、API、客戶端開發等,這些技術跨度很大,而它們隻有一個共同點:基於Node。

最常見的Node全棧開發形式就是MEAN——MongoDB、Express、AngularJS和Node。當然,全棧開發可以包含其他工具,例如在數據庫開發中使用MySQL或者Redis,以及除AngularJS以外的其他客戶端框架。而Express已經傢喻戶曉瞭。如果你要使用Node開發的話,必須熟悉Express。

 

MEAN的深入解讀

如果要對MEAN、全棧開發和Express進行更深入的學習,我推薦Ethan Brown的《Node與Express開發》、Shyam Seshadri和Brad Green的《用AngularJS開發下一代Web應用》和Scott Davis的視頻《MEAN技術棧架構》。

10.1 Express應用框架

在第5章中,我講瞭如何簡單地使用Node來構建一個Web應用程序。使用Node來創建Web應用很困難,所以像Express這樣的框架才會變得非常流行:它提供瞭我們需要的絕大部分功能,使我們的工作變得非常簡單。

有Node的地方,幾乎都有Express,所以一定要熟悉這個框架。我們在本章中會介紹最簡單的Express程序,但完成這些之後還需要進一步的訓練。

 

Express現在已經成為Node.js的基礎組件

Express一開始很不穩定,但現在已經是Node.js基礎組件之一。未來的開發應該會變得更穩定,功能也會更可靠。

Express有很好的文檔支持,包括如何啟動一個程序。我們會跟著文檔大綱一步一步來,然後擴展我們的基本程序。一開始,我們要為應用程序創建一個子目錄,起什麼名字無所謂。然後使用npm來創建一個package.json文件,並將app.js作為程序入口。最後,鍵入以下命令,安裝Express並保存到package.json的依賴中:

npm install express --save

Express的文檔包含瞭一個基本的Hello World程序,將下面的代碼放入app.js文件中:

var express = require('express');var app = express();app.get('/', function (req, res) {  res.send('Hello World!');}); app.listen(3000, function () {  console.log('Example app listening on port 3000!');});

app.get()函數會處理所有的GET請求,傳入我們在前面幾章已經很熟悉的request和response對象。按照慣例,Express程序會使用縮寫形式,也就是req和res。它們在默認的request和response對象的功能基礎上還加入瞭Express的功能。比如說,你可以調用res.write()和res.end()來為Web請求提供響應,如我們在前幾章中做過的一樣。但是有瞭Express,你就可以用res.send(),隻需一行就能實現同樣的功能。

我們還可以使用Express的生成器來生成程序框架,而不是手動創建。下面就會用到這個功能,它會提供一個功能更詳盡、可讀性更高的Express程序。

首先,全局安裝Express程序生成器:

sudo npm install express-generator –g

下一步,運行這個程序,後面跟上你想要創建的程序的名稱。此處我以bookapp為例:

express bookapp

Express程序生成器會創建所需的子目錄。然後進入bookapp子目錄安裝依賴:

npm install

好瞭,到此為止你的第一個Express程序框架就生成好瞭。如果你用的是OS X或者Linux環境,那麼可以使用下面的命令來運行程序:

DEBUG=bookapp:* npm start

如果是Windows則需要在命令行中運行下面的命令:

set DEBUG=bookapp:* & npm start

如果不需要調試的話,直接使用npm start也可以啟動程序。

程序啟動之後會在默認的3000端口上監聽請求。在瀏覽器中訪問程序,你會得到一個簡單的Web頁面,頁面上有一條歡迎語“Welcome to Express”。

程序會自動生成幾個子目錄和文件:

├── app.js├── bin│   └── www├── package.json├── public│   ├── images │   ├── javascripts│   └── stylesheets│       └── style.css├── routes│   ├── index.js│   └── users.js└── views    ├── error.jade    ├── index.jade    └── layout.jade

其中的很多組件我們都會講到,但是能夠公開訪問的文件都放在public子目錄中。你會註意到,圖片文件和CSS文件都在這個目錄中。動態內容的模板文件都在views目錄中。routes目錄包含瞭程序的Web接口,它們可以監聽Web請求和顯示Web頁面。

 

Jade現在已經更名為Pug

由於商標沖突,Jade的創始者無法再使用“Jade”作為Express和其他應用程序所使用的模板引擎的名稱瞭。但是,從Jade到Pug的轉換還在進行中。在生產環境,Express生成器仍將生成Jade文件,但是嘗試安裝Jade依賴的話則會產生一個錯誤信息:

Jade has been renamed to pug, please install the latest version of pug instead of jade

Pug的網站保留瞭Jade的名字,但是文檔和功能都是Pug的。

bin目錄下的www文件是程序的啟動腳本。它是一個被轉化為命令行程序的Node文件。如果查看生成的package.json文件,你會發現它出現在程序的啟動腳本中。

{  "name": "bookapp",  "version": "0.0.0",  "private": true,  "scripts": {    "start": "node ./bin/www"  },  "dependencies": {    "body-parser": "~1.13.2",    "cookie-parser": "~1.3.5",    "debug": "~2.2.0",    "express": "~4.13.1",    "jade": "~1.11.0",    "morgan": "~1.6.1",    "serve-favicon": "~2.3.0"  }}

你需要在bin目錄下安裝別的腳本,來對應用程序進行測試、重啟或其他控制。

現在讓我們來深入瞭解一下這個程序,就從程序的入口——app.js文件開始吧。

當你打開app.js文件時,你會看到裡面的代碼比我們之前看到的簡單程序還要多。代碼中引入瞭更多的模塊,其中大多數是為面向Web的應用程序提供中間件。被引入的模塊也會包含程序特定的引用,也就是routes目錄下的文件:

var express = require('express');var path = require('path');var favicon = require('serve-favicon');var logger = require('morgan');var cookieParser = require('cookie-parser');var bodyParser = require('body-parser');var routes = require('./routes/index');var users = require('./routes/users');var app = express();

其中所涉及的模塊以及它們的功能如下:

  • express,Express程序;
  • path,用來調用文件路徑的Node核心模塊;
  • serve-favicon,用來從給定的路徑或緩沖器提供favicon.ico文件的中間件;
  • morgon,一個HTTP請求日志記錄工具;
  • cookie-parser,解析cookie頭,並將結果填充到req.cookies;
  • body-parser,提供4種不同類型的請求內容解析器(除瞭multi-part類型的內容)。

每個中間件模塊都同時兼容普通的HTTP服務和Express服務。

 

什麼是中間件

中間件是我們的應用程序和系統、操作系統以及數據庫之間的橋梁。使用Express時,中間件就是應用程序鏈中的一部分,而每一部分都在完成與HTTP請求相關的特定功能——處理請求,或者對請求進行一些修改以便後面的中間件使用。Express所使用的中間件集合非常容易理解。

app.js中的下一段代碼,通過app.use()函數和給定的路徑加載中間件(也就是讓它們在程序中可用)。加載的順序同樣重要,所以如果你還需要加載別的中間件,一定要根據開發人員建議的順序進行添加。

這段代碼還包含瞭視圖引擎初始化的代碼,我稍後會講到。

// view engine setupapp.set('views', path.join(__dirname, 'views'));app.set('view engine', 'jade');// uncomment after placing your favicon in /public//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));app.use(logger('dev'));app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));app.use(cookieParser());app.use(express.static(path.join(__dirname, 'public')));

對app.use()的最後一次調用引用瞭為數不多的Express內建的中間件之一 —— express.static,它的作用是處理所有的靜態文件。如果一個Web用戶請求一個HTML、JPEG或者其他的靜態文件,這個請求就會由expess.static來處理。這個中間件加載之後,所有處於某個子目錄的相對路徑下的靜態文件都可供使用,在本例中,這個子目錄就是public。

回到app.set()函數調用,這個函數是用來定義視圖引擎的,你需要用一個模板引擎來幫你將數據展現給用戶。最流行的模板引擎之一 —— Jade會被默認加載,當然Mustache或者EJS也很好用。引擎的設置中會定義模板文件(視圖)所在的子目錄的位置,以及應該使用哪個視圖引擎(Jade)。

 

再次提醒:Jade現在叫Pug瞭

正如前面提到的,Jade現在叫Pug。Express文檔和Pug文檔你都需要查看一下,以便瞭解如何使用重新命名的模板引擎。

在本書付印之時,我修改瞭生成的package.json文件,將Jade替換為Pug:

"pug": "2.0.0-alpha8",

然後在app.js文件中,將jade引用替換為pug:

app.set('view engine', 'pug');

修改完成後整個應用程序運行起來沒有任何問題。

在views子目錄中,你會發現3個文件:error.jade、index.jade和layout.jade。這3個文件可以幫你初始化,當然還需要將數據集成到程序中。你需要做的遠不止這些。下面是生成的index.jade文件的內容:

extends layoutblock content  h1= title  p Welcome to #{title}

extends layout這一行會將layout.jade文件中的Jade語法集成進來。下面是HTML中的標題(h1)和段落(p)元素。h1標題被賦值為title,也就是被傳入模板的title變量,而title在段落元素中也用到瞭。這些值在模板中顯示的方式,決定瞭我們必須回到app.js文件並加入下面的代碼:

app.use('/', routes);app.use('/users', users);

這些都是程序特定的入口,也就是響應客戶請求的功能入口。根目錄的請求(‘/’)會被routes子目錄中的index.js文件處理。users請求會被users.js文件處理。

在index.js文件中,我們會接觸到Express路由(router),它提供瞭響應處理功能。Express文檔提到,路由的行為需要使用下面的模式來定義:

app.METHOD(PATH, HANDLER)

METHOD指的是HTTP方法,Express支持很多種方法,包括常見的get、post、put和delete,還有一些不常見的方法,比如search、head、options等。path指的是Web路徑,而handler指的是處理這個請求的函數。在index.js中,方法是get,path是程序的根路徑,而handler是一個傳遞請求和響應的回調函數:

var express = require('express');var router = express.Router();/* GET home page. */router.get('/', function(req, res, next) {  res.render('index', { title: 'Express' });});module.exports = router;

在res.render()函數中,數據(局部變量)和視圖將會被組合起來。這裡使用的視圖是我們前面看過的index.jade文件,你會發現模板中使用的title屬性的值,被作為數據傳遞給render函數。你可以在本地代碼中把Express改為任何你喜歡的內容,然後刷新頁面看看修改結果。

app.js文件中剩下的部分就都是錯誤處理瞭,這部分留給讀者自己分析理解。這是一個非常簡單和快速的Express示例,幸運的是麻雀雖小,五臟俱全,你可以從這個例子中瞭解一個Express程序的基本結構是什麼樣的。

 

數據整合

如果你想要瞭解如何在Express程序中進行數據整合,那麼我就拋磚引玉推薦一下我自己的書——《JavaScript經典實例》(譯版,中國電力出版社,2012年出版)。第14章展示瞭如何擴展一個現有的Express程序來集成MongoDB數據庫和控制器,從而實現一個完整的MVC架構。

10.2 MongoDB和Redis數據庫系統

在第7章中,例7-8展示瞭一個將數據插入MySQL數據庫的示例程序。雖然一開始比較粗糙,但是Node程序對關系型數據庫的支持越來越好瞭。比如Node對MySQL的穩定支持和用來在微軟Azure環境中訪問SQL Server的Tedious模塊。

Node同樣也支持另外一些數據庫系統。本節中我會簡單地介紹兩種數據庫:在Node開發中非常流行的MongoDB以及我個人最喜歡的Redis。

10.2.1 MongoDB

Node程序中最常見的數據庫就是MongoDB。MongoDB是一個基於文檔的數據庫。文檔被編碼為BSON格式——JSON的一種二進制編碼,或許這也是它在JavaScript中流行的原因。MongoDB用BSON文檔代替瞭數據表中的列,用集合代替瞭數據表。

MongoDB不是唯一一個文檔型數據庫。同樣類型的數據庫還有Apache的CouchDB和Amazon的SimpleDB、RavenDB,甚至還有傳奇的Lotus Notes。Node對各個現代數據庫的支持水平不一,但是MongoDB和CouchDB是支持得最好的。

MongoDB不是一個簡單的數據庫系統,而且在將它集成到你的程序中之前,你需要花一些時間來學習它的功能。然後,等你準備好瞭,你會發現Node中的MongoDB原生NodeJS驅動(MongoDB Native NodeJS Driver)對MongoDB的支持簡直是天衣無縫,而且你可以通過使用Mongoose來支持面向對象。

我不準備詳細介紹如何在Node中使用MongoDB,但是我會提供一個例子,以便你理解它的工作方式。雖然底層的數據結構與關系型數據庫不同,但是概念並沒有多大變化:你需要創建一個數據庫,然後創建一個數據集,向其中添加數據。這樣你可以更新、查詢或者刪除數據。在例10-1的MongoDB例子中,首先連接到一個示例數據庫,訪問一個叫Widgets的數據集,然後清空數據集,再插入兩條數據,最後將這兩條數據查詢出來並打印。

例10-1 使用MongoDB數據庫

var MongoClient = require('mongodb').MongoClient;// Connect to the dbMongoClient.connect("mongodb://localhost:27017/exampleDb",                                        function(err, db) {   if(err) { return console.error(err); }   // access or create widgets collection   db.collection('widgets', function(err, collection) {      if (err) return console.error(err);      // remove all widgets documents      collection.remove(null,{safe : true}, function(err, result) {         if (err) return console.error(err);         console.log('result of remove ' + result.result);         // create two records         var widget1 = {title : 'First Great widget',                         desc : 'greatest widget of all',                         price : 14.99};         var widget2 = {title : 'Second Great widget',                         desc : 'second greatest widget of all',                         price : 29.99};         collection.insertOne(widget1, {w:1}, function (err, result) {            if (err) return console.error(err);            console.log(result.insertedId);            collection.insertOne(widget2, {w:1}, function(err, result) {               if (err) return console.error(err);               console.log(result.insertedId);               collection.find({}).toArray(function(err,docs) {                  console.log('found documents');                  console.dir(docs);                  //close database                  db.close();               });            });         });       });   }); });

是的,代碼中又出現瞭Node的回調地獄。你可以使用promise來規避它。

MongoClient對象就是我們連接數據庫時所使用的對象。註意給出的端口號(27017)。這是MongoDB的默認端口號。我們所使用的數據庫是exampleDB,寫在連接URL中。我們所使用的數據集是widgets,用它來紀念開發者所熟知的Widget類。

意料之中的是,MongoDB的函數都是異步的。數據被插入之前,我們的程序會先在不使用查詢語句的情況下調用collection.remove(),來刪除數據集中的所有記錄。如果不這樣做,數據庫就會存在重復記錄,因為MongoDB會對每條新數據都賦予一個系統生成的唯一標識符,而我們也沒有指定title或者其他字段為唯一標識符。

然後,我們調用collection.insertOne()來創建新數據,將定義對象的JSON作為參數傳入。選項{w:1}表示寫入策略(write concern),是MongoDB中寫操作的響應級別。

數據被插入以後,我們的程序再次使用collection.find(),同樣不帶查詢參數,來查詢所有數據。這個函數實際上會創建一個指針,然後toArray()函數會將指針指向的內容生成一個數組返回。我們後面可以用console.dir()函數將它的內容打印出來。程序執行的結果會類似於下面的內容:

result of remove 156c5f535c51f1b8d712b655256c5f535c51f1b8d712b6553found documents[ { _id: ObjectID { _bsontype: 'ObjectID', id: 'VÅõ5Å\\u001f\\u001bq+eR' },   title: 'First Great widget',   desc: 'greatest widget of all',   price: 14.99 },  { _id: ObjectID { _bsontype: 'ObjectID', id: 'VÅõ5Å\\u001f\\u001bq+eS' },   title: 'Second Great widget',   desc: 'second greatest widget of all',   price: 29.99 } ]

每個對象的標識符其實也是一個對象,而且是BSON格式的,所以打印出來的都是亂碼。如果想要去掉亂碼,你可以分別打印對象中的每個字段,然後使用toHexString()對BSON格式的內容進行轉碼:

docs.forEach(function(doc) {                    console.log('ID : ' + doc._id.toHexString());                    console.log('desc : ' + doc.desc);                    console.log('title : ' + doc.title);                    console.log('price : ' + doc.price);                 });

最後的結果就成瞭:

result of remove 156c5fa40d36a4e7b72bfbef256c5fa40d36a4e7b72bfbef3found documentsID : 56c5fa40d36a4e7b72bfbef2desc : greatest widget of alltitle : First Great widgetprice : 14.99ID : 56c5fa40d36a4e7b72bfbef3desc : second greatest widget of alltitle : Second Great widgetprice : 29.99

你可以使用命令行工具來查看MongoDB數據庫中的數據。按照下面的順序調用命令就可以啟動工具並查看數據。

(1)輸入mongo啟動命令行工具。

(2)輸入use exampleDb切換到exampleDb數據庫。

(3)輸入show collections查看所有的數據集。

(4)輸入db.widgets.find()來查看Widget中的所有數據。

如果你想要用一個基於對象的方式來集成MongoDB,那麼Mongoose就是你要找的東西。如果要集成到Express,Mongoose也許是一個更好的選擇。

不用MongoDB的時候,記得將它關閉。

 

Node文檔中的MongoDB相關內容

Node的MongoDB驅動有在線的文檔可以查看,你可以通過GitHub代碼庫來訪問這個文檔,也可以在MongoDB的網站上看到這個文檔。我更推薦新手使用MongoDB網站上的文檔。

10.2.2 Redis中的key/value存儲

數據庫有兩種,一種是關系型數據庫,另一種是非關系型數據庫,而非關系型數據庫,就是我們所說的NoSQL。在所有的NoSQL數據庫中,有一種基於鍵/值(key/value)的數據結構,通常存儲在內存中,從而能夠提供極快的訪問速度。3種最流行的基於內存的key/value存儲分別是Memcached、Cassandra和Redis。Node開發人員應該感到高興,因為Node對這3種存儲都提供瞭支持。

Memcached主要用於緩存數據查詢從而能快速訪問內存中的數據。將它用於分佈式計算也是一個不錯的選擇,隻是它對復雜數據的支持有限。對於需要執行大量查詢的應用程序,Memcached非常有用,但對於有大量數據寫入和讀取的應用程序來說則略遜一籌。對於後一種應用程序,Redis則是一個超棒的選擇。Redis可以持久化,此外,它比Memcached提供瞭更多的靈活性,特別是在支持不同類型的數據時。美中不足的是,與Memcached不同,Redis隻能在一臺機器上工作。

Redis和Cassandra則比較相似。和Memcached一樣的是,Cassandra支持集群。不一樣的是,它對數據結構的支持有限。Cassandra對於ad hoc查詢非常有用,Redis則不然。不過Redis使用簡單,不復雜,而且要比Cassandra快很多。出於各種各樣的原因,Redis在Node開發人員中獲得瞭更多的關註。

 

EARN

EARN(Express、AngularJS、Redis和Node)這個縮寫讓人讀起來很有感覺。在The EARN Stack中有一個關於EARN的例子。

我推薦使用Node中的Redis模塊,用npm就可以安裝:

npm install redis

如果你打算在Redis上進行一些大型操作,我還建議安裝Node模塊支持hiredis,因為它是非阻塞的,可以提高性能:

npm install hiredis redis

Redis模塊隻對Redis進行瞭一層簡單的封裝。因此,你需要自己花時間學習Redis命令以及Redis數據存儲的工作原理。

在Node應用中使用Redis時,要先引入模塊:

var redis = require('redis');

接著需要使用createClient方法創建一個Redis客戶端:

var client = redis.createClient();

createClient方法有3個可選的參數:port、host和options(稍後講解)。默認的host是127.0.0.1,port是6379。這個端口就是Redis服務器的默認端口,所以如果Redis服務器與Node應用運行在同一臺機器上,那麼使用默認設置就可以工作。

第3個參數是一個對象,它支持一些選項,Redis模塊的文檔中有詳細介紹。在熟悉Node和Redis前,使用默認設置就可以瞭。

一旦客戶端連接到Redis數據庫,你就可以給服務器發送命令瞭,直到調用client.quit()方法關閉應用程序與Redis服務的連接。如果想要強制關閉,可以使用client.end()方法。不過,後一種方法並不會等所有的返回值都被解析才斷開。如果應用程序無響應或者你想重新開始運行程序,就可以使用client.end()。

通過客戶端連接發送Redis命令是一個相當直觀的過程。所有命令都作為客戶端對象上的方法暴露出來,而所有命令的參數都可以作為方法的參數傳遞。由於這是Node,所以最後一個參數是一個回調函數,回調函數的參數是一個錯誤對象和Redis命令的返回結果。

在下面的代碼中,我們用client.hset()方法設置瞭一個hash屬性。在Redis中,hash是字符串格式的字段和值的映射(mapping),比如“lastname”對應姓氏,而“firstname”對應名字,以此類推:

client.hset("hashid", "propname", "propvalue", function(err, reply) {   // do something with error or reply});

hset命令是用來設置值的,沒有返回數據,因為存在Redis裡面瞭。如果調用一個能獲取多個值的方法,如client.hvals,則回調函數中的第二個參數將是一個數組——可以是字符串數組或對象數組:

client.hvals(obj.member, function (err, replies) {   if (err) {      return console.error("error response - " + err);   }   console.log(replies.length + " replies:");   replies.forEach(function (reply, i) {     console.log("    " + i + ": " + reply);   });});

由於Node的回調函數很普及,且很多Redis命令都是返回成功確認的操作,因此Redis模塊提供瞭redis.print方法,該方法可以作為回調函數的最後一個參數傳入:

client.set("somekey", "somevalue", redis.print);

redis.print函數會將錯誤信息或者控制臺中返回的內容打印出來,然後返回。

為瞭在Node中演示Redis,我創建瞭一個消息隊列(message queue)。消息隊列是一種應用程序,它將某種形式的通信作為輸入,然後存儲到隊列中。消息一直存儲在隊列中,直到被接收方取走,此時消息會被移出隊列,發送給接收方(每次一條或者批量進行)。通信是異步的,因為存儲消息的應用不要求接收器保持連接,接收器也不要求消息存儲應用保持連接。

Redis是這種應用的理想存儲介質。當消息被存儲它們的應用程序接收時,它們被添加到隊尾。當消息被接收它們的應用程序取出時,它們將從隊首取出。

 

瞭解一些TCP、HTTP和子進程相關的知識

這個Redis的例子由一個TCP服務器(因此使用瞭Node的Net模塊)、一個HTTP服務器和一個子進程組成。第5章介紹瞭HTTP,第7章介紹瞭Net,第8章介紹瞭子進程。

在演示消息隊列時,我創建瞭一個Node應用程序來訪問幾個不同子域名下的Web日志文件。應用程序用瞭Node子進程和UNIX的tail-f命令來訪問不同日志文件的最新記錄。

在訪問這些日志記錄時,應用程序使用瞭兩個正則表達式對象:第一個用來提取訪問到的資源的內容,第二個用來檢測資源是否為圖片文件。如果被訪問的資源是圖片文件,應用程序就把該資源的URL通過TCP消息發送到消息隊列的應用程序中。

消息隊列程序所做的事情就是在3000端口監聽消息,然後將接收到的所有內容都發送到Redis數據庫進行存儲。

示例程序的第三部分是一個在8124端口監聽請求的Web服務器。對於每個請求,它都會訪問Redis數據庫並取出圖像數據庫中靠前的記錄,通過響應對象返回這條記錄。如果Redis數據庫在請求圖片資源時返回null,則會打印出一條消息,表明應用程序已到達消息隊列的末尾。

程序的第一部分在處理Web日志記錄,如例10-2所示。UNIX的tail命令可以顯示文本文件(或管道中的數據)的最後幾行。當加上-f參數時,將會顯示文件中幾行然後暫停,並監聽新的日志記錄。一旦有新的記錄,它就會將其打印出來。tail –f也可以用於需要同時監聽多個文件的情況,它可以通過給數據打標簽(標出其來源)的方式來管理這些內容。這個命令並不關心最新的記錄來自哪個文件——它隻關心日志本身。

一旦程序拿到瞭日志(log),它就會對數據進行正則表達式匹配,從而發現可以訪問的圖片資源(文件擴展名為.jpg、.gif、.svg或者.png)。如果匹配成功,就把資源URL發送到消息隊列程序(一個TCP服務器)。程序很簡單,它不會去檢查字符串到底是文件後綴名還是嵌入在文件名中,比如this.jpg.html。對於這樣的文件名,你會得到一個假陽性(false positive)結果。不過隻要它能演示Redis的用法就夠瞭。

例10-2 處理Web日志並將圖片資源請求發送到消息隊列的Node程序

var spawn = require('child_process').spawn;var net = require('net');var client = new net.Socket();client.setEncoding('utf8');// connect to TCP serverclient.connect ('3000','examples.burningbird.net', function() {    console.log('connected to server');});// start child processvar logs = spawn('tail', ['-f',        '/home/main/logs/access.log',        '/home/tech/logs/access.log',        '/home/shelleypowers/logs/access.log',        '/home/green/logs/access.log',        '/home/puppies/logs/access.log']);// process child process datalogs.stdout.setEncoding('utf8');logs.stdout.on('data', function(data) {   // resource URL   var re = /GET\s(\S+)\sHTTP/g;   // graphics test   var re2 = /\.gif|\.png|\.jpg|\.svg/;   // extract URL   var parts = re.exec(data);   console.log(parts[1]);   // look for image and if found, store   var tst = re2.test(parts[1]);   if (tst) {      client.write(parts[1]);   }});logs.stderr.on('data', function(data) {   console.log('stderr: ' + data);});logs.on('exit', function(code) {   console.log('child process exited with code ' + code);   client.end();});

這個程序會輸出如下所示的典型的控制臺日志記錄,需要關註的部分(圖片文件訪問)已用粗體標出:

/robots.txt/weblog/writings/fiction?page=10/images/kite.jpg/node/145/culture/book-reviews/silkworm/feed/atom//images/visitmologo.jpg/images/canvas.png/sites/default/files/paws.png/feeds/atom.xml

例10-3包含瞭消息隊列的代碼。這個簡單的程序會啟動一個TCP服務器然後監聽發送來的消息。當它接收到消息時,會抽取其中的數據存儲到Redis數據庫中。這個程序用Redis的rpush命令將數據存入圖片列表的末尾(在代碼中加粗標出)。

例10-3 接收消息並將它存入Redis列表的消息隊列

var net = require('net');var redis = require('redis'); var server = net.createServer(function(conn) {   console.log('connected');    // create Redis client   var client = redis.createClient();    client.on('error', function(err) {     console.log('Error ' + err);   });     // sixth database is image queue   client.select(6);   // listen for incoming data   conn.on('data', function(data) {      console.log(data + ' from ' + conn.remoteAddress + ' ' +        conn.remotePort);       // store data       client.rpush('images',data);   });  }).listen(3000);server.on('close', function(err) {   client.quit(); });  console.log('listening on port 3000');

下面是消息隊列程序的控制臺日志:

listening on port 3000connected/images/venus.png from 173.255.206.103 39519/images/kite.jpg from 173.255.206.103 39519/images/visitmologo.jpg from 173.255.206.103 39519/images/canvas.png from 173.255.206.103 39519/sites/default/files/paws.png from 173.255.206.103 39519

消息隊列程序的最後一個需要演示的功能是監聽8124端口的HTTP服務器,如例10-4所示。每當HTTP服務器接收到一個請求,它都會訪問Redis數據庫,取出圖片列表中的下一條記錄,並打印到響應(response)中。如果隊列中沒有內容瞭(例如,Redis返回null),則返回一條消息說消息隊列為空。

例10-4 從Redis列表中取出信息並將它返回給HTTP服務器

var redis = require("redis"),    http = require('http');var messageServer = http.createServer();// listen for incoming requestmessageServer.on('request', function (req, res) {   // first filter out icon request   if (req.url === '/favicon.ico') {      res.writeHead(200, {'Content-Type': 'image/x-icon'} );      res.end();      return;   }    // create Redis client   var client = redis.createClient();   client.on('error', function (err) {     console.log('Error ' + err);   });    // set database to 6, the image queue   client.select(6);   client.lpop('images', function(err, reply) {      if(err) {         return console.error('error response ' + err);      }      // if data      if (reply) {         res.write(reply + '\n');      } else {         res.write('End of queue\n');      }      res.end();   });   client.quit();});messageServer.listen(8124);console.log('listening on 8124');

通過瀏覽器訪問HTTP服務器時,每個請求都會返回一個圖片資源URL,直到消息隊列為空。

這個例子涉及的數據很簡單,但可能非常多,這也是它適合使用Redis的原因。Redis是一個快速、簡單的數據庫,而且不用花費太多精力就能將它集成到Node程序中。

何時創建Redis客戶端

當我使用Redis時,有時候會創建一個Redis客戶端讓它始終存在於程序中,而有時則在Redis命令結束後就釋放之前創建的Redis客戶端。那麼什麼時候應該創建一個持久的Redis連接?什麼時候又該建立連接並在結束使用後立即釋放呢?

好問題。

為瞭測試這兩種不同的策略,我創建瞭一個TCP服務器,用來監聽請求(request)並一個將簡單的散列值存入Redis數據庫。接著我創建瞭另一個應用程序作為TCP客戶端,它隻負責將對象搭載在TCP消息中發送給服務器。

我用ApacheBench程序並發運行一些客戶端,並重復這個過程,每次運行後測試其運行時間。首先運行那些使用瞭持久Redis連接的程序,接著運行那些為每個請求建立數據庫連接、但使用之後就立即釋放連接的程序。

我期望的測試結果是擁有持久化客戶端連接的程序運行較快,結果證明在某種程度上,我是對的。大約在測試到一半的時候,建立持久連接的應用程序在一段很短的時間內處理速度急劇降低,然後恢復瞭相對較快的速度。

當然,最可能發生的情況是,在隊列中等待的Redis數據庫請求最終會(至少是短暫的)阻塞Node程序,直到隊列被清空。而每一次請求都需要打開和關閉連接時,並不會發生類似的情況,因為這個過程所需的額外開銷會減慢應用程序的運行速度,剛好沒有達到數據庫並發訪問的上限。

本文截選自《 Node學習指南》第2版

學習Node全棧開發,你都需要掌握哪些知識?

[美] 謝利·鮑爾斯(Shelley Powers) 著,曹隆凱,婁佳 譯

  • 深入淺出node.js開發實戰教程
  • JavaScript技術作傢力作,教你實現快速和高度可擴展的網絡應用
  • 針對Node6.0和長期支持版本

本書是學習Node編程的入門指南。全書共12章,由淺入深。本書首先介紹Node的基礎知識、Node的核心功能、Node的模塊系統和REPL等,然後講解Node的Web應用、流和管道、Node對文件系統的支持、網絡和套接字、子進程、ES6等相關知識,最後介紹瞭全棧Node編程、Node的開發環境和產品環境以及Node的新應用。
本書適合有一定基礎的JavaScript程序員閱讀,也適合對學習Node應用開發感興趣的讀者學習參考。

Published in News by Awesome.

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *