本書絕大部分內容關注的都是組成Node的核心模組和功能。我之所以儘量避免使用第三方模組,是因為Node還是一個很不穩定的環境,所以它對第三方模組的支援會隨著時間的推移而發生快速且劇烈的變化。
但是我覺得,如果不了解更廣泛的Node應用上下文的話,我們就無法真正掌握Node。換句話說,你需要熟悉Node全棧開發。這意味著你需要熟悉資料系統、API、客戶端開發等,這些技術跨度很大,而它們只有一個共同點:基於Node。
最常見的Node全棧開發形式就是MEAN——MongoDB、Express、AngularJS和Node。當然,全棧開發可以包含其他工具,例如在資料庫開發中使用MySQL或者Redis,以及除AngularJS以外的其他客戶端框架。而Express已經家喻戶曉了。如果你要使用Node開發的話,必須熟悉Express。
10.1Express應用框架MEAN的深入解讀
如果要對MEAN、全棧開發和Express進行更深入的學習,我推薦Ethan Brown的《Node與Express開發》、Shyam Seshadri和Brad Green的《用AngularJS開發下一代Web應用》和Scott Davis的視訊《MEAN技術棧架構》。
在第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:
<p>"pug": "2.0.0-alpha8",</p>
然後在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程式的基本結構是什麼樣的。
10.2MongoDB和Redis資料庫系統資料整合
如果你想要了解如何在Express程式中進行資料整合,那麼我就拋磚引玉推薦一下我自己的書——《JavaScript經典例項》(譯版,中國電力出版社,2012年出版)。第14章展示了如何擴充套件一個現有的Express程式來整合MongoDB資料庫和控制器,從而實現一個完整的MVC架構。
在第7章中,例7-8展示了一個將資料插入MySQL資料庫的示例程式。雖然一開始比較粗糙,但是Node程式對關係型資料庫的支援越來越好了。比如Node對MySQL的穩定支援和用來在微軟Azure環境中訪問SQL Server的Tedious模組。
Node同樣也支援另外一些資料庫系統。本節中我會簡單地介紹兩種資料庫:在Node開發中非常流行的MongoDB以及我個人最喜歡的Redis。
10.2.1MongoDBNode程式中最常見的資料庫就是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的時候,記得將它關閉。
10.2.2Redis中的key/value儲存Node文件中的MongoDB相關內容
Node的MongoDB驅動有線上的文件可以檢視,你可以通過GitHub程式碼庫來訪問這個文件,也可以在MongoDB的網站上看到這個文件。我更推薦新手使用MongoDB網站上的文件。
資料庫有兩種,一種是關係型資料庫,另一種是非關係型資料庫,而非關係型資料庫,就是我們所說的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版
[美] 謝利·鮑爾斯(Shelley Powers) 著,曹隆凱,婁佳 譯
深入淺出node.js開發實戰教程JavaScript技術作家力作,教你實現快速和高度可擴充套件的網路應用針對Node6.0和長期支援版本本書是學習Node程式設計的入門指南。全書共12章,由淺入深。本書首先介紹Node的基礎知識、Node的核心功能、Node的模組系統和REPL等,然後講解Node的Web應用、流和管道、Node對檔案系統的支援、網路和套接字、子程序、ES6等相關知識,最後介紹了全棧Node程式設計、Node的開發環境和產品環境以及Node的新應用。本書適合有一定基礎的JavaScript程式設計師閱讀,也適合對學習Node應用開發感興趣的讀者學習參考。