Node.js是什麼
Node讓你能夠用javascript編寫服務器端程序,讓javascript脫離web瀏覽器的限制,像C#、JAVA、Python等語言同樣在服務器端運行,這也讓一些熟悉Javascript的前端開發人員進軍到服務器端開發提供了一個便利的途徑。 Node是基於Google的V8引擎封裝的,並提供了一些編寫服務器程序的經常使用接口,例如文件流的處理。Node的目的是提供一種簡單的途徑來編寫高性能的網絡程序。
(注:一、本文基於Node.js V0.3.6; 二、本文假設你瞭解JavaScript; 三、本文假設你瞭解MVC框架;四、本文示例源代碼:learnNode.zip)javascript
Node.js的性能
300併發請求,返回不一樣大小的內容:
爲何node有如此高的性能?看node的特性。html
Node.js的特性
更詳細的瞭解node請看淘寶UED博客上的關於node.js的一個幻燈片:http://www.slideshare.net/lijing00333/node-jsjava
你好,世界
這,固然是俗套的Hello World啦(hello_world.js):node
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');
require相似於C#的using、Python的import,用於導入模塊(module)。node使用的是CommonJS的模塊系統。http.createServer 的參數爲一個函數,每當有新的請求進來的時候,就會觸發這個函數。最後就是綁定要監聽的端口。git
怎麼運行?
固然,是先安裝node.js啦。到http://nodejs.org/下載並編譯,支持Linux、Mac,也支持windows下的Cygwin。具體的安裝說明見:http://howtonode.org/how-to-install-nodejs 裝好node後,就能夠運行咱們的hello world了:github
$ node hello_world.js Server running at http://127.0.0.1:8124/
編程習慣的改變?
咱們來寫一個讀取文件內容的腳本:web
//output_me.js var fs = require('fs'), fileContent = 'nothing'; fs.readFile(__filename, "utf-8", function(err, file) { if(err) { console.log(err); return; } fileContent = file; console.log('end readfile \n'); }); console.log('doSomethingWithFile: '+ fileContent +'\n');
這個腳本讀取當前文件的內容並輸出。__filename是node的一個全局變量,值爲當前文件的絕對路徑。咱們執行這個腳本看一下:
ajax
有沒發現結果不對呢?打印的fileContent並非讀取到的文件內容,而是初始化的時候賦值的nothing,而且‘end readfile’最後纔打印出來。前面咱們提到node的一個特性就是非阻塞IO,而readFile就是異步非阻塞讀取文件內容的,因此後面的代碼並不會等到文件內容讀取完了再執行。請謹記node的異步非阻塞IO特性。因此咱們須要將上面的代碼修改成以下就能正常工做了:
//output_me.js var fs = require('fs'), fileContent = 'nothing'; fs.readFile(__filename, "utf-8", function(err, file) { if(err) { console.log(err); return; } fileContent = file; //對於file的處理放到回調函數這裏處理 console.log('doSomethingWithFile: '+ fileContent +'\n'); }); console.log('咱們先去喝杯茶\n');
寫個Web MVC框架試試
下面咱們用node來寫一個小玩具:一個Web MVC框架。這個小玩具我稱它爲n2Mvc,它的代碼結構看起來大概以下:
和hello world同樣,咱們須要一個http的服務器來處理全部進來的請求:
var http = require('http'), querystring = require("querystring"); exports.runServer = function(port){ port = port || 8080; var server = http.createServer(function(req, res){ var _postData = ''; //on用於添加一個監聽函數到一個特定的事件 req.on('data', function(chunk) { _postData += chunk; }) .on('end', function() { req.post = querystring.parse(_postData); handlerRequest(req, res); }); }).listen(port); console.log('Server running at http://127.0.0.1:'+ port +'/'); };
這裏定義了一個runServer的方法來啓動咱們的n2Mvc的服務器。有沒注意到runServer前面有個exports?這個exports至關於C#中的publish,在用require導入這個模塊的時候,runServer能夠被訪問到。咱們寫一個腳原本演示下node的模塊導入系統:
//moduleExample.js var myPrivate = '豔照,藏着'; exports.myPublish = '冠西的相機'; this.myPublish2 = 'this也能夠哦'; console.log('moduleExample.js loaded \n');
從結果中咱們能夠看出exports和this下的變量在外部導入模塊後,能夠被外部訪問到,而var定義的變量只能在腳本內部訪問。 從結果咱們還能夠看出,第二次require導入moduleExample模塊的時候,並無打印「moduleExample.js loaded」,由於require導入模塊的時候,會先從require.cache 中檢查模塊是否已經加載,若是沒有加載,纔會從硬盤中查找模塊腳本並加載。 require支持相對路徑查找模塊,例如上面代碼中require(‘./moduleExample’)中的「./」就表明在當前目錄下查找。若是不是至關路徑,例如 require(‘http’),node則會到require.paths中去查找,例如個人系統require.paths爲:
當require(‘http’)的時候,node的查找路徑爲:
1、/home/qleelulu/.node_modules/http 2、/home/qleelulu/.node_modules/http.js 3、/home/qleelulu/.node_modules/http.node 4、/home/qleelulu/.node_modules/http/index.js 5、/home/qleelulu/.node_modules/http/index.node 6、/home/qleelulu/.node_libraries/http 7、/home/qleelulu/.node_libraries/http.js 8、參考前面
再看回前面的代碼,http.createServer中的回調函數中的request註冊了兩個事件,前面提到過node的一個特色是事件驅動的,因此這種事件綁定你會處處看到(想一想jQuery的事件綁定?例如$(‘a’).click(fn))。關於node的事件咱們在後面再細說。request對象的data事件會在接收客戶端post上來的數據時候觸發,而end事件則會在最後觸發。因此咱們在data事件裏面處理接收到的數據(例如post過來的form表單數據),在end事件裏面經過handlerRequest 函數來統一處理全部的請求並分發給相應的controller action處理。 handlerRequest的代碼以下:
var route = require('./route');
var handlerRequest = function(req, res){
//經過route來獲取controller和action信息
var actionInfo = route.getActionInfo(req.url, req.method);
//若是route中有匹配的action,則分發給對應的action
if(actionInfo.action){
//假設controller都放到當前目錄的controllers目錄裏面,還記得require是怎麼搜索module的麼?
var controller = require('./controllers/'+actionInfo.controller); // ./controllers/blog
if(controller[actionInfo.action]){
var ct = new controllerContext(req, res);
//動態調用,動態語言就是方便啊
//經過apply將controller的上下文對象傳遞給action
controller[actionInfo.action].apply(ct, actionInfo.args);
}else{
handler500(req, res, 'Error: controller "' + actionInfo.controller + '" without action "' + actionInfo.action + '"')
}
}else{
//若是route沒有匹配到,則看成靜態文件處理
staticFileServer(req, res);
}
};
這裏導入來一個route模塊,route根據請求的url等信息去獲取獲取controller和action的信息,若是獲取到,則經過動態調用調用action方法,若是沒有匹配的action信息,則做爲靜態文件處理。 下面是route模塊的代碼:
var parseURL = require('url').parse;
//根據http請求的method來分別保存route規則
var routes = {get:[], post:[], head:[], put:[], delete:[]};
/**
* 註冊route規則
* 示例:
* route.map({
* method:'post',
* url: /\/blog\/post\/(\d+)\/?$/i,
* controller: 'blog',
* action: 'showBlogPost'
* })
*/
exports.map = function(dict){
if(dict && dict.url && dict.controller){
var method = dict.method ? dict.method.toLowerCase() : 'get';
routes[method].push({
u: dict.url, //url匹配正則
c: dict.controller,
a: dict.action || 'index'
});
}
};
exports.getActionInfo = function(url, method){
var r = {controller:null, action:null, args:null},
method = method ? method.toLowerCase() : 'get',
// url: /blog/index?page=1 ,則pathname爲: /blog/index
pathname = parseURL(url).pathname;
var m_routes = routes[method];
for(var i in m_routes){
//正則匹配
r.args = m_routes[i].u.exec(pathname);
if(r.args){
r.controller = m_routes[i].c;
r.action = m_routes[i].a;
r.args.shift(); //第一個值爲匹配到的整個url,去掉
break;
}
}
//若是匹配到route,r大概是 {controller:'blog', action:'index', args:['1']}
return r;
};
map方法用於註冊路由規則,咱們新建一個config.js的文件,來配置route規則:
//config.js
var route = require('./route');
route.map({
method:'get',
url: /\/blog\/?$/i,
controller: 'blog',
action: 'index'
});
若是請求的url有匹配的route規則,則會返回controller和action信息。例如上面的route配置,當訪問 /blog 這個url的時候,則會調用 ./controllers/blog.js 模塊裏面的index函數。 當調用action的時候,會傳遞controllerContext給acation:
var ct = new controllerContext(req, res);
//動態調用,動態語言就是方便啊
//經過apply將controller的上下文對象傳遞給action
controller[actionInfo.action].apply(ct, actionInfo.args);
這裏會經過apply將controllerContext做爲action的this,並傳遞args做爲action的參數來調用action。 ontrollerContext封裝了一些action會用到的方法:
//controller的上下文對象
var controllerContext = function(req, res){
this.req = req;
this.res = res;
this.handler404 = handler404;
this.handler500 = handler500;
};
controllerContext.prototype.render = function(viewName, context){
viewEngine.render(this.req, this.res, viewName, context);
};
controllerContext.prototype.renderJson = function(json){
viewEngine.renderJson(this.req, this.res, json);
};
在action中處理完邏輯獲取獲取到用戶須要的數據後,就要呈現給用戶。這就須要viewEngine來處理了。ViewEngine的代碼以下:
var viewEngine = {
render: function(req, res, viewName, context){
var filename = path.join(__dirname, 'views', viewName);
try{
var output = Shotenjin.renderView(filename, context);
}catch(err){
handler500(req, res, err);
return;
}
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(output);
},
renderJson: function(res, json){
//TODO:
}
};
這裏viewEngine主要負責模板解析。node有不少的可用的模塊,模板解析模塊也有一大堆,不過這裏咱們是要「玩」,因此模板解析系統咱們這裏使用jstenjin來稍做修改:
//shotenjin.js 增長的代碼
//模板緩存,緩存解析後的模板
Shotenjin.templateCatch = {};
//讀取模板內容
//在模板中引用模板使用: {# ../layout.html #}
Shotenjin.getTemplateStr = function(filename){
//console.log('get template:' + filename);
var t = '';
//這裏使用的是同步讀取
if(path.existsSync(filename)){
t = fs.readFileSync(filename, 'utf-8');
}else{
throw 'View: ' + filename + ' not exists';
}
t = t.replace(/\{#[\s]*([\.\/\w\-]+)[\s]*#\}/ig, function(m, g1) {
var fp = path.join(filename, g1.trim())
return Shotenjin.getTemplateStr(fp);
});
return t;
};
Shotenjin.renderView = function(viewPath, context) {
var template = Shotenjin.templateCatch[viewPath];
if(!template){
var template_str = Shotenjin.getTemplateStr(viewPath);
var template = new Shotenjin.Template();
template.convert(template_str);
//添加到緩存中
Shotenjin.templateCatch[viewPath] = template;
}
var output = template.render(context);
return output;
};
global.Shotenjin = Shotenjin;
增長的代碼主要是讀取模板的內容,並解析模板中相似 {# ../layout.html #} 的標籤,遞歸讀取全部的模板內容,而後調用jstenjin的方法來解析模板。 這裏讀取文件內容使用的是fs.readFileSync,這是同步阻塞讀取文件內容的,和咱們平時使用的大多編程語言同樣,而fs.readFile的非阻塞異步讀。 這裏的shotenjin.js原來是給客戶端web瀏覽器javascript解析模板用的,如今拿到node.js來用,徹底不用修改就正常工做。Google V8真威武。 如今基本的東西都完成了,可是對於靜態文件,例如js、css等咱們須要一個靜態文件服務器:
var staticFileServer = function(req, res, filePath){
if(!filePath){
filePath = path.join(__dirname, config.staticFileDir, url.parse(req.url).pathname);
}
path.exists(filePath, function(exists) {
if(!exists) {
handler404(req, res);
return;
}
fs.readFile(filePath, "binary", function(err, file) {
if(err) {
handler500(req, res, err);
return;
}
var ext = path.extname(filePath);
ext = ext ? ext.slice(1) : 'html';
res.writeHead(200, {'Content-Type': contentTypes[ext] || 'text/html'});
res.write(file, "binary");
res.end();
});
});
};
var contentTypes = {
"aiff": "audio/x-aiff",
"arj": "application/x-arj-compressed"
//省略
}
簡單來講就是讀取文件內容並寫入到response中返回給客戶端。 如今該有的都有了,咱們寫一個action:
// ./controllers/blog.js
exports.index = function(){
this.render('blog/index.html', {msg:'Hello World'});
};
blog/index.html的內容爲:
{# ../../header.html #} <h3 class="title">n2Mvc Demo</h3> <h1>#{msg}</h1> {# ../../footer.html #}
接着,就是寫一個腳原本啓動咱們的n2Mvc了:
// run.js
var n2MvcServer = require('./server');
n2MvcServer.runServer();
嗯嗯,一切正常。 好,接下來咱們再寫一個獲取新浪微博最新微博的頁面。首先,咱們在config.js中增長一個route配置:
route.map({
method:'get',
url: /\/tweets\/?$/i,
controller: 'blog',
action: 'tweets'
});
而後開始寫咱們的cnotroller action:
var http = require('http'),
events = require("events");
var tsina_client = http.createClient(80, "api.t.sina.com.cn");
var tweets_emitter = new events.EventEmitter();
// action: tweets
exports.tweets = function(blogType){
var _t = this;
var listener = tweets_emitter.once("tweets", function(tweets) {
_t.render('blog/tweets.html', {tweets: tweets});
});
get_tweets();
};
function get_tweets() {
var request = tsina_client.request("GET", "/statuses/public_timeline.json?source=3243248798", {"host": "api.t.sina.com.cn"});
request.addListener("response", function(response) {
var body = "";
response.addListener("data", function(data) {
body += data;
});
response.addListener("end", function() {
var tweets = JSON.parse(body);
if(tweets.length > 0) {
console.log('get tweets \n');
tweets_emitter.emit("tweets", tweets);
}
});
});
request.end();
}
這裏使用http.createClient來發送請求獲取新浪微博的最新微博,而後註冊相應事件的監聽。這裏詳細說下node的事件系統:EventEmitter。 EventEmitter能夠經過require(‘events’). EventEmitter來訪問,建立一個 EventEmitter的實例emitter後,就能夠經過這個emitter來註冊、刪除、發出事件了。 例如上面的代碼中,先建立來一個EventEmitter的實例:
var tweets_emitter = new events.EventEmitter();
而後用once註冊一個一次性的事件監聽:
var listener = tweets_emitter.once("tweets", function(tweets) {
_t.render('blog/tweets_data.html', {tweets: tweets});
});
once註冊的事件在事件被觸發一次後,就會自動移除。 最後,經過emit來發出事件:
tweets_emitter.emit("tweets", tweets);
這樣,整個事件的流程都清晰了。 下面寫一下顯示tweets的模板:
<ul> <?js for(var i in tweets){ ?> <?js var tweet = tweets[i], user = tweets[i].user; ?> <li> <div class="usericon"> <a class="user_head" href="###"> <img src="#{user.profile_image_url}" /> </a> </div> <div class="mainContent"> <div class="userName"> <a href="###"> #{user.screen_name} </a> </div> <div class="msg"