只在此山中,雲深不知處


聽首歌



© 2018 by Shawn Huang
Last Updated: 2018.5.27

Nodejs Introduction

Node.js是伺服器端的程式,使用JavaScript的語法。若要使用,首先須先安裝,前往Nodejs官方網站,若是Windows系統,直接看到兩個下載檔案,一個是Recommended For Most Users,一個是Latest Features,下載任一個皆可。下載之後點擊檔案即可安裝,原則上一路按next即可。完成之後應可在開始選單中看到如下畫面:

安裝完成之後,接下來測試一下。首先建立一個新的資料夾作為工作環境空間(e.g. E:\Node\Node_Demo),接著開啟Nodepad++,然後到node.js的官方網頁上的About頁面,copy當頁的程式碼貼到Nodepad++,先別管內容的意思,如下:

將檔案存檔(e.g. node1.js)。開啟DOS視窗(在開始工具列打cmd,或是如上圖的Node.js處點擊Node.js command prompt),改變路徑至檔案儲存之資料夾(e.g. cd Node\Node_Demo),然後使用執行檔案之關鍵字node node1.js

接下來開啟網頁瀏覽器頁面,在位址欄打上http://127.0.0.1:3000/(或http://localhost:3000),可以看到網頁出現Hello World,完成,此時伺服器開始持續運作,若要終止伺服器,按Ctrl+C。

REPL


REPL是Read-Eval-Print-Loop的縮寫,類似終端機可以接受命令輸入,可以點擊開始工具列中Node.js執行檔,或是在DOS介面輸入node,即可開始使用。

你可以嘗試使用JavaScript語法。

Modules

Modules就是類似函式庫,我們可以將其導入然後使用,可以直接使用內建提供之函式庫,也可以自行建立,使用時使用關鍵字require()函數來引入。將部分程式碼寫入另一個檔案可以讓我們的主要程式碼不會過於冗長,而且便於管理與偵錯,並且可以重複使用。

自訂module


自訂module的方式就是先寫好要使用的函數於另一個檔案中,之後再使用關鍵字require導入即可,首先開啟一個module2_1.js的檔案,在其中建立以下函數:
exports.Hello = function(){
	console.log("Hello World.");
}
 
exports.DateTime = ()=>{
	return Date();
}

注意要使用exports關鍵字來輸出函數,將此檔案儲存於同一個資料夾(Node_Demo)。也可以寫成以下形式,原則上是相同的。若是private函數或是任何不需要export的函數,只要不要加上exports即可,沒有被exports的函數在主程式便無法使用。
function Hello(){
	console.log("Hello World.");
}
var DateTime = ()=>{
	return Date();
}
module.exports.Hello = Hello;
module.exports.DateTime = DateTime;

事實上整個exports是一個物件,所以也可以使用以下方式,只要將函數一個一個寫進去物件即可,甚至可以加上參數(e.g. moduleName)。
module.exports = {
	Hello:function (){
		console.log("Hello World.");
	},
	DateTime: ()=>{
		return Date();
	},
	moduleName: "My First Module"
}

接下來建立主要的nodejs檔案(e.g. node2_1.js),在其中使用以下程式碼:
const myModule = require("./module2_1");
myModule.Hello();
console.log(myModule.DateTime());
可看到第一行使用require函數導入module2_1這個module,請注意之前須有./,表示是在同一個資料夾內的module。導入後指派給變數myModule,因為沒有要改變,可以使用const,也可以使用var(or let)。第二行與第三行便是直接呼叫myModule內的函數即可。一樣在DOS介面內輸入node node2_1.js,可以看到輸出結果。
在剛剛的module裡,加上下列程式碼:
exports.Warrior = {
	name: "",
	weapon: ""
}

exports.Mage = function(damage){
	return {
		name: "",
		weapon: "",
		attact: damage
	}
}
第一個Warrior是一個物件,第二個Mage則傳回一個物件,在主程式中輸入以下程式碼:
let tom = myModule.Warrior;
tom.name = "Tom";

let aaron = myModule.Warrior;
aaron.weapon = "Axe";

console.log(tom.name, tom.weapon);
console.log(aaron.name, aaron.weapon);

let mary = myModule.Mage(100);
mary.name = "Mary";
mary.weapon = "Staff";
console.log(mary.name, mary.weapon, mary.attact);

let helen = myModule.Mage(200);
helen.name = "Helen";
helen.weapon = "Wand";
console.log(helen.name, helen.weapon, helen.attact);
結果顯示tom跟aaron的結果相同,顯然兩者共享同一個物件,而mary與helen則不相同,兩者為獨立的物件,我們可以藉由如此方式來分享或建立物件。

Core modules (Built in modules)


Core modules (或Built in modules)是node.js的內建函式庫,也就是我們安裝好node.js後可以直接使用的module,因為是內建的,所以直接使用require(moduleName)即可導入,不像自訂module還要考慮檔案路徑。以下介紹幾個例子,詳細列表可以參考官網

path & url


>>
const path = require('path');

let invalidpath = "C://Courses\\Nodejs/////node1.js"
let filepath = path.normalize(invalidpath);

console.log("filepath: ", filepath);
console.log("dirname: ", path.dirname(filepath));
console.log("basename: ", path.basename(filepath));
console.log("extname: ", path.extname(filepath));

let urlObj = path.parse(filepath);
console.log("dir: ", urlObj.dir);
console.log("root: ", urlObj.root);
console.log("base: ", urlObj.base);
console.log("name: ", urlObj.name);
console.log("ext: ", urlObj.ext);

這個例子使用path module,直接使用require()函數導入。normalize()函數可以幫我們正常化路徑,在不同的作業系統,路徑的表示可能有不同,有的時候是斜線(\)有的時候是反斜線(/),這個函數可以轉化為目前系統的型態。dirname(), basename(), extname()這三個函數分別傳回C:\Course\Nodejs, node1.js, 與.js,看結果應該就知道指路徑的哪個部位。
parse()這個函數可以將路徑轉換為物件,其中包含dir, root, base, name, ext等變數,看輸出即可知其意義。其他path方法可以參考官網
>>
const url = require('url');
let theurl = 'http://localhost:8080/default.htm?year=2018&month=January';
let urlobj = url.parse(theurl, true);

console.log("host: ", urlobj.host);
console.log("hostname: ", urlobj.hostname);
console.log("pathname: ", urlobj.pathname);
console.log("search: ", urlobj.search);
這個例子使用url module,一樣使用require()函數導入即可,使用parse()函數將其轉為物件,根據輸出可以看出host, hostname, pathname, search等參數所代表的意義。

os


os是跟作業系統相關的函數集,使用方式與前同。
>>
const os = require('os');

console.log("cpu: ", os.cpus());
console.log("free memory: ", os.freemem());
console.log("host name: ", os.hostname());
console.log("operating system type: ", os.type());
console.log("user info: ", os.userInfo());
console.log("platform: ", os.platform());	

使用os函數可以得到跟operating system有關的資訊。

file system(fs)


fs顯然跟檔案系統有關,直接看以下例子:
>>
var fs = require('fs');
fs.writeFileSync("temptxt.txt", "隨便寫點東西在這裡");
fs.appendFileSync("temptxt.txt", "\n再多寫一行");
console.log(fs.readFileSync("temptxt.txt").toString(), "Read file completes.");	
導入fs之後,使用writeFileSync()函數將txt寫到temptxt.txt檔案內,再使用appendFileSync()函數增加內容,之後使用readFileSync()函數讀出檔案內容並印出。注意讀出後要使用toString()函數,否則會出現一堆二進位數值內容。
>>
fs.writeFile("temptxt1.txt", "隨便寫點東西在這裡1", function(err){
	if(err)
		throw err;
	console.log("Write to file completed.1");
});
fs.appendFile("temptxt1.txt", "\n再多寫一行1", (err)=>{
	if(err)
		throw err;
	console.log("Append file completed.1");
});
fs.readFile("temptxt1.txt", function(err, data){
	if(err)
		throw err;
	console.log(data.toString());
});	
加上這些程式碼後,期待跟上列的程式碼出現一樣的結果,不過事實上這段程式碼的輸出卻可能沒有照著順序出現。原因是這些函數是Asynchronous,而之前的函數是Syncrhonous(Sync)。Syncrhonous(同步)的意思是當我們呼叫該函數,該函數完成後,才會去進行其他步驟。而Asynchronous(異步)的函數被呼叫後,不需等待該步驟完成,便可以去進行其他步驟。通常Asynchronous的函數會被賦予一個參數為callback函數,當函數完成要求後,接著會執行callback函數,在函數要求尚未被滿足前,程式可能先去執行其他部分的程式碼,等到偵測到函數要求被滿足後,接下來才要執行callback函數的程式碼。通常這個callback函數的第一個參數為err(Error)。至於應該要使用哪一種函數來實現我們的目的似乎見仁見智,如果是小檔案,不需要霸占整個流程,使用Asynchronous,若是不擔心霸占整個流程,可以使用Syncrhonous。
若要照步驟完成,可以將下一步寫進前一步的callback內。
>>
fs.writeFile("temptxt2.txt", "隨便寫點東西在這裡2", function(err){
	if(err)
		throw err;
	console.log("Write to file completed.2");
	
	fs.appendFile("temptxt2.txt", "\n再多寫一行2", (err)=>{
		if(err)
			throw err;
		console.log("Append file completed.2");
		fs.readFile("temptxt2.txt", function(err, data){
			if(err)
				throw err;
			console.log(data.toString());
		});
	});
});	

若要取得文件的統計資料,可以使用fs.stat()方法傳回fs.Stats物件。
>>
fs.stat('temptxt2.txt', (err, stats)=>{
	if(err){
		return console.error(err);
	}
	var writeData = stats;
	console.log(writeData);
	console.log("isFile ? " + stats.isFile());
	console.log("isDirectory ? " + stats.isDirectory());
	console.log("Got file info successfully!");
});

若要改檔案名,使用rename()。
>>
fs.renameSync("temptxt.txt", "temptxt99.txt");
fs.rename("temptxt1.txt", "temptxt100.txt", (err)=>{
	if(err)
		throw err;
	console.log("Rename completed.");
});	
一樣一個是Syncrhonous,另一個是Asynchronous。
而若要刪除檔案,則使用unlink()。
>>
fs.unlinkSync("temptxt99.txt");
fs.unlink("temptxt100.txt", function(err){
	if(err)
		throw err;
	console.log("Delete file completed.");
});	
也是一樣一個是Syncrhonous,另一個是Asynchronous。
WriteStream & ReadStream --> see Events&Stream

Global Objects


Global Objects是在所有module內都可以被取得的物件,例如console。以下介紹幾個常見的例子。
>>
global.console.log("Dir:",__dirname);
global.console.log("Filename:",__filename);
__dirname與__filename分別會得到目前目錄與檔案名稱。
>>
setTimeout(function(){
	console.log("Hello, Stormwind City.");
},3000);
setTimeout()這個函數的語法是setTimeout(callback, delay[, ...args]),第一個參數callback是要做的事情,第二個參數delay是延遲時間,單位是毫秒,所以3000是3秒的意思。執行上述程式碼後會經過3秒才見到輸出。
>>
setInterval(()=>{
	console.log("Hi, Ironforge City.");
}, 2000);
setInterval(callback, delay[, ...args])這個函數是每經過delay時間後便會重複執行callback函數。
>>
let st = setTimeout(function(){
	console.log("Hello, Stormwind City.");
},3000);
clearTimeout(st);
重寫上面setTimeout的code,使用clearTimeout()函數將會中止setTimeout函數。
>>
let t = 0;
let si = setInterval(()=>{
	if(t < 5){
		console.log("Hi, Ironforge City.", t);
		t++;
	}else{
		clearInterval(si);
	}
}, 1000);
再重寫上面setInterval的code,使用clearInterval()函數將會中止setTimeout函數。

現在試一下以下的例子:
>>
console.log("Hello");
setTimeout(()=>{
	console.log("Hi, Ironforge City.");
}, 2000);
console.log("Stormwind City.");
若是將setTimeout()的delay改為0秒,可以發現setTimeout()內的內容還是最後才被執行。因為asynchronous的callback會先被安排至一個task queue,等其他程式碼被執行後,才會依序執行task queue內的事件,所以會在之後執行。

HTTP

HTTP也是屬於Core module,它可以讓我們建立HTTP Server。其實在一開始驗證安裝是否完成時便做過一次,現在可以理解其中意義了。
>> node3_1.js
const http = require('http');

const server = http.createServer(function(req, res){
	res.end('Hello World!');
})

server.listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
首先當然要require('http'),然後使用createServer()函數,此函數會傳回http.Server物件。參數是一個函數,該函數自動加到requst事件,所以會有兩個參數,一個request,一個response。request屬於http.IncomingMessage物件,response屬於http.ServerResponse物件。此處直接呼叫end()結束,但是給予字串作為參數值,表示以此為結束。最後呼叫server.listen方法在特定port或路徑建立listener,語法是server.listen(port, hostname, backlog, callback);參數都是optional,此處選擇3000作為port number。如一開始所說的,開始執行後,在瀏覽器上打localhost:3000可以看到輸出結果。
我們可以使用setHeader函數來設定內容型態的值,所以可以將程式碼改寫為:
const http = require('http');

const server = http.createServer(function(req, res){
	res.setHeader('Content-Type','text/plain');
	res.write('Hi, there,');
	res.end('Hello World!');
})

server.listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
res.write()方法是將參數作為response輸出,而setHeader方法設定response內容為純文字(text/plain),因為內容都是文字,所以沒有甚麼差別,若是我們將res.write的內容改為res.write('<h1>Hi, there,</h1>');結果會顯示出<h1>Hi, there,</h1>因此若是要顯示出html的內容,需要設定header使其為html,如下:
const http = require('http');

const server = http.createServer(function(req, res){
	res.setHeader('Content-Type','text/html');
	res.write('<h1>Hi, there,</h1>');
	res.end('Hello World!');
})

server.listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});

Server的request將網址列訊息儲存在url參數,我們可以使用之前說過的url module來parse。
>> node3_2.js
const http = require('http');
const url = require('url');

const server = http.createServer(function(req, res){
	res.setHeader('Content-Type','text/html');
	let q = url.parse(req.url, true).query;
	res.write('<h1>Hi, ' + q.castle + '</h1>');
	res.write('I am a ' + q.occupation);
	res.end();
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
你也可以試著印出res.write(req.url);的內容便知道指的是哪個部分。

readFile


我們可以利用之前的readFile方式來讀取其他檔案內容來顯示到Server,首先先建立一個html檔案如下。
>> IronForgeCity.html
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h1>鐵爐堡</h1>
<p>矮人工匠冶煉精鐵中...</p>
</body>
</html>
接下來建立nodejs檔案。
>> node3_3.js
const http = require('http');
const url = require('url');
const fs = require('fs');

http.createServer(function(req, res){
	var q = url.parse(req.url, true);
	var filename = "."+q.pathname;
	
	fs.readFile(filename, (err,data)=>{
		if(err){
			res.writeHead(404,{'Content-Type':'text/html'});
			return res.end("\n404 Not Found");
		}
		res.writeHead(200,{'Content-Type':'text/html'});
		res.write(data);
		res.end();
	});
	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
writeHead函數的語法是response.writeHead(statusCode[, statusMessage][, headers]);其中statusCode是一個三碼的數字,例如404代表有問題,200代表正常。statusMessage是optional,第三個是header物件,也是optional。
若是要讀取的是JSON資料,可以以下方式。
>> node3_3_json.js
const http = require('http');
const fs = require('fs');
http.createServer(function(req, res){
	res.writeHead(200, {'Content-Type':'application/json'});
	let player = {
		name: 'Helen',
		occupation: "Mage",
		skills: ['fireball', 'lightning', 'nova']
	};
	res.end(JSON.stringify(player));
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});

NPM


NPM是Node Package Manager的簡稱,可以協助我們管理package,在安裝node.js的時候已經一併安裝,所以可以直接使用。 更多詳情可以參考這裡

Event

在電腦內每一個動作都是一個event,例如開啟檔案或是新增連結。物件可以觸發event,在node.js內有一個core module稱為events,可用來處理event。
>> node4_1.js
const events = require('events');
let eventEmitter = new events.EventEmitter();
let magefire = function(){
	console.log('Fireball...');
}
eventEmitter.on('fire', magefire);
eventEmitter.emit('fire');
好像發射砲彈一樣,首先require events這個module,使用new關鍵字建立EventEmitter()物件<<發射器>>,使用關鍵字on來連結event名稱<<按鈕名稱>>與對應處理函數<<砲彈>>。然後使用emit函數<<按下按鈕>> 觸發事件。
>> node4_2.js
const events = require('events');
const util = require('util');//for inherit
let mage = function(name){//物件
	this.name = name;
}

util.inherits(mage, events.EventEmitter);//inherit
let magefire = (name, nOfFire)=>{
	console.log(name + " 施法發出 "+ nOfFire + " 發火球");
}

let Helen = new mage("法師海倫");
Helen.on('fire', magefire);
Helen.emit('fire',Helen.name,3);

let Tom = new mage("法師湯姆");
Tom.on('fire', magefire);
Tom.emit('fire',Tom.name,5);
這個例子讓物件繼承events.EventEmitter使其成為EventEmitter物件,然後使用該物件來觸發事件。node.js的util module提供繼承的方法inherits,可以直接方便的使用。之後只要new新的物件,讓其on綁定事件處理方法,然後emit即可觸發。
繼承物件也可以使用JavaScript的方式。
>> node4_3_module.js
const events = require('events');
class mage extends events.EventEmitter{
	constructor(name){
		super();
		this.name = name;
	}
	magefire(name, nOfFire){
		console.log(name + " 施法發出 "+ nOfFire + " 發火球");	
	}
}
module.exports = mage;	

先寫一個module其中包含繼承自events.EventEmitter的物件,然後exports。
>> node4_3.js
const Mage = require("./node4_3_module");
const helen = new Mage("法師海倫");
helen.on('fire', helen.magefire);
helen.emit('fire',helen.name, 3);	
將module導入後,使用其中的class建立物件,然後on跟emit來觸發。
接下來試著將其寫入到http server內。先再建立一個html檔案。
>> StormWindCity.html
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<style>
	p{
		color: blue;
		background: lime;
	}
</style>
<body>
	<h1>暴風城</h1>
	<p>人類法師施法中...</p>
</body>
</html>
將module修改如下。
>> node4_4_module.js
const events = require('events');
class mage extends events.EventEmitter{
	constructor(name){
		super();
		this.name = name;
	}
	magefire(res, nOfFire){
		res.write("<span style='color:blue;'>" + this.name + "</span>" + " 施法發出 "+"<span style='color:red;'>" + nOfFire+ "</span>" + " 發火球");	
	}
}
module.exports = mage;
修改一下要導入的module。
>> node4_4.js
const http = require('http');
const fs = require('fs');
const url = require('url');
const Mage = require("./node4_4_module");

const helen = new Mage('法師海倫');
helen.on('fire', helen.magefire);
	
http.createServer(function(req, res){
	let q = url.parse(req.url, true);
	let filename = "."+q.pathname;
	
	fs.readFile(filename, (err,data)=>{
		if(err){
			res.writeHead(404,{'Content-Type':'text/html'});
			return res.end("\n404 Not Found");
		}
		res.writeHead(200,{'Content-Type':'text/html'});
		res.write(data);
		if(filename == "./StormWindCity.html"){
			helen.emit('fire',res,3);
		}
		res.end();
	});
	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
跟之前一樣導入後建立物件,僅在filename == "./StormWindCity.html"為true時emit,可以之前介紹過的使用nodemon node4_4.js指令,若有錯誤可以直接修改檔案,無須斷線重連。在http://localhost:3000/StormWindCity.html可以看到輸出結果。

Stream


使用fs內的readFile(writeFile)時,基本上是將所有資料讀入記憶體,然後一股腦兒進行下一步處理(callback),如果資料量很大的時候,可能會將所有資源耗費在其上,stream的資料傳輸方式則是讀取少量資料(chunk)至buffer,待buffer滿了之後便開始處理,等清空後再繼續讀取,這樣可以較有效率地將資料傳遞。在fs內提供WriteStream與ReadStream供我們使用。
>> node4_5.js
const fs = require('fs');
let readstream = fs.createReadStream('./temptxt2.txt', 'utf-8');
readstream.on('data', (chunk)=>{
	console.log('New chunk: ',chunk);
});
createReadStream會傳回一個fs.ReadStream物件,它是一種Readable Streams,每次一個chunk的資料轉移,便會emit事件。需要加上utf-8參數,否則顯示出來的為二進位碼。如果讀進來的資料想要寫進另一個檔案裡,則可以使用createWriteStream()。
>>
const fs = require('fs');
let readstream = fs.createReadStream('./temptxt2.txt', 'utf-8');
let writestream = fs.createWriteStream('./newfile.txt');
readstream.on('data', (chunk)=>{
	writestream.write(chunk);
});
這是一個連續動作,讀進然後寫出,node.js提供串流指令pipe來串聯這整個系列動作。
>> node4_5_pipe.js
const fs = require('fs');
let readstream = fs.createReadStream(__dirname +'/temptxt2.txt', 'utf-8');
let writestream = fs.createWriteStream(__dirname + '/newfile.txt');
readstream.pipe(writestream);
若是要將內容寫入http server內,可以使用readstream.pipe(res)指令來完成。
>> node4_5_httppipe.js
const http = require('http');
const fs = require('fs');
http.createServer((req,res)=>{
	res.writeHead(200, {'Content-Type':'text/plain;charset=utf-8'});
	let readstream = fs.createReadStream(__dirname +'/temptxt2.txt', 'utf-8');
	readstream.pipe(res);	
	//res.end();//Don't need this this time
}).listen(3000,()=>{
	console.log("Server is running on port 3000...");
});
若是跟之前類似,要讀的檔案是html並要將其顯示出來(e.g. http://localhost:3000/StormWindCity.html),可以採取如下方式。
>> node4_5_httppipehtml.js
const http = require('http');
const fs = require('fs');
http.createServer((req,res)=>{
	res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
	let readstream = fs.createReadStream(__dirname + req.url, 'utf-8');
	readstream.on('error',(err)=>{
		res.writeHead(404,{'Content-Type':'text/html'});
		return res.end("\n404 Not Found");
	});	
	readstream.pipe(res);
}).listen(3000,()=>{
	console.log("Server is running on port 3000...");
});

Routing

http server可能需要控制多個request,此時可以使用if...else...語法來處理。
>> node5_1_routing.js
const http = require('http');
const fs = require('fs');
const path = require('path');
http.createServer(function(req, res){
	if(req.url === '/'){
		res.write('<h1>World of Warcraft</h1>');
		res.end();
	}
	else if(req.url === '/Cities/StormWindCity'){
		console.log(req.url);		
		res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
		let readstream = fs.createReadStream(path.normalize(__dirname + req.url)+".html", 'utf-8');
		readstream.pipe(res);
	}
	else if(req.url === '/Cities/IronForgeCity'){
		res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
		let readstream = fs.createReadStream(path.normalize(__dirname + req.url)+".html", 'utf-8');
		readstream.pipe(res);
	}
	else if(req.url === '/Cities/Danasus'){
		console.log(req.url);
		res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
		let readstream = fs.createReadStream(path.normalize(__dirname + req.url)+".html", 'utf-8');
		readstream.pipe(res);
	}
	else{
		res.writeHead(404,{'Content-Type':'text/html'});
		res.end("Page Not Found.");
	}
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
不過當要處理的request形態越來越多時,程式碼也就越來越複雜了,屆時維護會愈來越困難。此時可以使用Express。
上面的程式碼可以簡化如下,好像也沒那麼複雜= =...不過這裡只是簡單顯示內容,若再加上其他操作還是會變得越來越龐雜。
>> node5_1a_routing.js
const http = require('http');
const fs = require('fs');
const path = require('path');
http.createServer(function(req, res){
	if(req.url === '/'){
		res.write('<h1>World of Warcraft</h1>');
		res.end();
	}
	else {		
		res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
		let readstream = fs.createReadStream(path.normalize(__dirname + req.url)+".html", 'utf-8');
		readstream.on('error', function(){
			res.writeHead(404,{'Content-Type':'text/html'});
			res.end("Page Not Found.");
		});
		readstream.pipe(res);
	}
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});

Form


我們可以自表單得到資訊,介紹常見的get與post。
>> node5_2_get.js
const http = require('http');
http.createServer(function(req, res){
	res.writeHead(200, {'content-type':'text/html'});
	res.write('<form action="/" enctype="multipart/form-data" method="get"><br>');
	res.write('<input type="text" name="name"><br>');
	res.write('<input type="text" name="occupation"><br>');
	res.write('<input type="submit">');
	res.write('</form>');
	res.write(req.url);
	return res.end();	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
使用get來取得資料,會出現在網址列,這樣的資料較不安全。再看若使用post的情況。
>> node5_2_post.js
const http = require('http');
const qs = require('querystring');
http.createServer(function(req, res){
	if(req.url=="/"){
		res.writeHead(200, {'content-type':'text/html'});
		res.write('<form action="/One" method="post"><br>');
		res.write('<input type="text" name="name"><br>');
		res.write('<input type="text" name="occupation"><br>');
		res.write('<input type="submit">');
		res.write('</form>');
		res.end();
	}
	if(req.url=="/One"){
		let body = '';
		req.on('data', function(data){
			body += data;
		});
		req.on('end', function(){
			var post = qs.parse(body);
			console.log(post['name'], post['occupation']);
			res.writeHead(200, {'content-type':'text/html'});
			res.write(`<span style="color: blue">${post['name']}</span> <span style="color: red">${post['occupation']}</span> is added.`);
			res.end();
		});
	}
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
此處使用querystring module來parse傳來的資料,parse後為一物件。需使用req.on('data',callback)來收集資料,再使用req.on('end',callback)來結束。

Express


使用Express需要先下載package,因為會下載一大堆相關package,所以可能先建立一個新的資料夾再下載比較不會搞混。改變路徑到新建立的資料夾,使用以下指令: 通常會專注於以下幾個routing方法,更多方法請見官網

Get



>> index.js
const express = require('express');
const app = express();

app.get('/', function(req, res){
	res.send('<h1>World of Warcraft</h1>');// can be called only once, equal to res.write()+res.end()
	//res.write('<h1>World of Warcraft</h1>');
	//res.end();
});

// PORT (environment variable)
const port = process.env.PORT || 3000;
app.listen(port, ()=>{
	console.log(`Listening on port ${port}.......`); // change the single quote mark
});
執行express()產生名為app的物件,使用get方法取得路徑'/',callback函數內使用res.send(),此為res.write()+res.end(),僅用於express。process.env.PORT是自動取得可取得的PORT號碼。可以看到此方式比之之前的routing似乎容易管理一些。
似乎接下來要加上以下程式碼來回應對應的request。
>>
app.get('/Cities/StormWindCity', function(req, res){
	res.send("<h1>暴風城</h1>");
});
這樣雖然可以,不過當有很多城市時,又要一一列舉,所以可使用以下方式。
>>
app.get('/Cities/:cityname', function(req, res){
	res.send(`<h1>${req.params.cityname}</h1>`);
});
:cityname是輸入變數,使用req.params.cityname取得變數值,${req.params.cityname}是讓我們可以在字串內顯示變數值,記得要將字串的引號(')或雙引號(")換成(`)才能使用。
若是要讀取檔案再顯示,還是可以使用ReadStream方式。
>>
app.get('/Cities/:cityname', function(req, res){	
	let readstream = fs.createReadStream(path.normalize(__dirname + req.url)+".html", 'utf-8');
	readstream.pipe(res);
});
不過在express內提供另一個函數sendFile可供使用。
>>
app.get('/Cities/:cityname', function(req, res){	
	res.sendFile(__dirname + "/Cities/" +req.params.cityname+".html");
});

接下來加上以下的code。
>>
var players = [
	{id:1, name: 'Helen', occupation:'戰士'},
	{id:2, name: 'Tom', occupation:'法師'}
];
app.get('/Players', function(req, res){
	res.send(players);
});
app.get('/Players/:id', (req, res)=>{
	let hasrole = players.find(c => c.id===parseInt(req.params.id));
	if(!hasrole){ //404
		return res.status(404).send(`The role ${req.params.id} is unavailable`);
	}else{
		let index = req.params.id-1;
		res.status(200).send(`Hi, 我是<span style="color:blue;">${players[index].name}</span>, \n 我是一個<span style="color:green;">${players[index].occupation}</span>`);
	}
});
這裡設計一個名為players的array,裡面的物件是player。第一個get('/')方法可以顯示所有的玩家,第二個get('/Players/:id')會根據id顯示個別玩家。

Post


使用post取得資料,資料可使用body-parser module來parse,所以先使用npm install body-parse來安裝body-parser,記得要將其request。(const bodyParser = require("body-parser");)
>>
app.use(bodyParser.urlencoded({
	extended: true
}));

app.get('/Players', (req, res)=>{
	res.writeHead(200, {'content-type':'text/html'});
	res.write('<form action="/Players/" method="post"><br>');
	res.write('<input type="text" name="name"><br>');
	res.write('<input type="text" name="occupation"><br>');
	res.write('<input type="submit">');
	res.write('</form>');
	res.end();
});

app.post('/Players', function(req, res){
	let newRole = {
		id: players.length+1,
		name: req.body.name,
		occupation: req.body.occupation
	};
	console.log(newRole);
	players.push(newRole);
	res.send(newRole);
});
使用app.use()來使用bodyParser的功能,並用app.get()來設計一個post表單,action對應app.post()要回應的位置,在app.post的callback內使用req.body來取得參數。

Upload

先下載formidable module,使用npm install formidable指令。
>> node6_1_formidable.js
const http = require('http');
const formidable = require('formidable');
const util = require('util');
const fs = require('fs');
http.createServer(function(req, res){
	if(req.url == '/upload'){
		let form = new formidable.IncomingForm();
		
		form.parse(req, (err, fields, files)=>{
			if (err)
				throw err;
			let oldpath = files.fileupload.path;
			let newpath = 'C:/Temp/' + files.fileupload.name;
			fs.rename(oldpath, newpath, (err)=>{
				if (err)
					throw err;
				res.writeHead(200, {'content-type':'text/html'});
				res.write('file uploaded');
				res.write(util.inspect({fields: fields, files: files}));
				res.end();
			});
		});
		return;
	}else{
		res.writeHead(200, {'content-type':'text/html'});
		res.write('<form action="/upload" enctype="multipart/form-data" method="post"><br>');
		res.write('<input type="file" name="fileupload"><br>');
		res.write('<input type="submit" value="Upload">');
		res.write('</form>');
		return res.end();
	}
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});	
在else內建立表單,method也是post,action為/upload,也就是submit後會跳到/upload次路徑,使用type='file'會出現選擇檔案的按鈕。在req.url=='/upload'情況下,使用formidable.IncomingForm()取得物件,使用其parse(req, callback)方法即可得到內容。因為會自動上傳到某目錄(C:\users\...),而且在該位置上傳的檔案會自動被命名,所以使用fs.rename來將檔案導引到特定資料夾目錄,並保留原檔案檔名。也就是屆時檔案會儲存到C:/Temp/目錄內。util.inspect()是內建module util的方法,可將物件內容轉為描述字串。

Email


使用nodemailer module,使用npm install nodemailer下載。
>> node6_2_email.js
const http = require('http');
const nodemailer = require('nodemailer');
const qs = require('querystring');
http.createServer(function(req, res){
	if(req.url == '/'){
		res.writeHead(200, {'content-type':'text/html'});
		res.write('<form action="/email" method="post"><br><br>');
		res.write('From: <input type="text" name="from"><br><br>');
		res.write('To: <input type="text" name="to"><br><br>');
		res.write('Subject: <input type="text" name="subject"><br><br>');
		res.write('Content: <textarea type="text" rows="20" cols="100" name="content">&lt;/textarea&gt;<br><br>');
		res.write('<input type="submit" value="Send">');
		res.write('</form>');
		res.end();
	}
	if(req.url == '/email'){
		let body = '';
		req.on('data', function(data){
			body += data;
		});
		req.on('end', function(){
			var post = qs.parse(body);
			let transporter = nodemailer.createTransport({
				service:'gmail',
				auth:{
					user: post["from"],
					pass: 'yourPassword'
				}
			});
				
			let mailOptions = {
				from: post["from"],
				to: post["to"],
				subject: post["subject"],
				text: post["content"] // Send plain text
				//html: `<h1>${post["content"]}</h1>`// Send out html content
			};
			transporter.sendMail(mailOptions, function(error, info){
				if(error){
					console.log(error);
				}else{
					res.writeHead(200,{'Content-Type':'text/html'});
					res.write("Mail Sent."+info.response);
					res.end();
				}
			});
		});	
	}
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
首先在req.url == '/'頁面建立email表單格式,接著在req.url == '/email'進行回應,一樣使用querystring來取得表單內容資料。email的部分先建立transporter,使用nodemailer.ccreateTransport,此處採用gmail的郵件主機服務,pass使用寄信信箱的密碼。mailOption包含from, to, subject, text(亦可為html)等內容,然後使用transporter.sendMail(mailOptions, callback)方法寄送。<<此程式碼只能使用特定service,且要給定密碼。>>

MongoDB

首先下載安裝MongoDB,接下來 安裝MongoDB driver,使用npm install mongodb指令。找到mongodb的安裝目錄(e.g. C:\Program Files\MongoDB\Server\3.6\bin),執行mongod.ext(打mongod)來啟動mongodb,若是連結失敗,d可能需要檢查一下是否有這個目錄(根據連線失敗訊息)C:\Data\db,再啟動一次(mongod)。
>> node7_1_mongodb.js
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/mydb";

mongoClient.connect(url, function(err, db){
	if (err) throw err;
	console.log("Database created!");
	db.close();
});	
跟之前一樣執行node node7_1_mongodb.js,出現Database created!字樣,成功建立一個database名為mydb。

Collection


Collection就是一個表格的意思,使用createCollection()方法建立。
>> node7_2_collection.js
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		mydb.createCollection("players", (err,result)=>{
			if (err) 
				throw err;
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			res.write("Collection created!");
			res.end();			
			db.close();
		});
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
成功建立一個collection。

insert


增加一筆資料進入collection。
>> node7_3_insert.js
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

let helen = {name: "Helen", occupation: "Priest"};
http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		mydb.collection("players").insertOne(helen, (err,result)=>{
			if (err) 
				throw err;
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			res.write("The following is inserted:\n");
			res.write('<span style="color:blue">'+helen.name+'</span>\t' + '<span style="color:green">'+helen.occupation+'</span>');
			res.end();
			db.close();
		});
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
會出現E11000 duplicate key error index這個錯誤訊息然後斷線,雖然還是insert成功..........

find


findOne(query, callback)會傳回第一筆找到的資料,query是搜尋條件,下例因為給空物件,所以會傳回第一筆資料。。
>> node7_4_find.js
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		mydb.collection("players").findOne({}, (err,result)=>{
			if (err) 
				throw err;
			console.log(result.name);
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			res.write(`<span style="color:blue">${result.name}</span> found.`);
			res.end();
			db.close();
		});
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
若要尋找特定條件資料,使用find(query).toArray(callback)方法(toArray是因為可能找到不只一筆資料),若要限制傳回數量可以加上limit(n)例如find(query).limit(5).toArray(callback)
>> node7_5_find.js
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		let query={name:"Mary"};
		//let query={name:/M./};
		mydb.collection("players").find(query).toArray((err,result)=>{
			if (err) 
				throw err;
			console.log(result);
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			for (i of result){
				res.write(`<span style="color:blue">${i.name}, ${i.occupation}</span> found.<br>`);
			}
			res.end();
			db.close();
		});
		
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
query的內容可以使用Regular Expression,例如使用query={name:/M./};來找到name是M開頭的資料。

Update


>> node7_6_update.js
<
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		//let query={name:"Mary"};
		let query={name:/M./};
		let newvalue = {$set:{occupation: "Dark Knight"}};
		mydb.collection("players").updateOne(query, newvalue, (err,results)=>{
			if (err) 
				throw err;
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			res.write(results.result.nModified + " records are modified.<br>");
			res.end();
			db.close();
		});
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});
若使用updateMany則會更新所以符合的資料。

sort


可以使用find(query).sort({item:1}).toArray(callback)來排序,使用find(query)可以先找到需要的資料,若無query則為全部資料。sort({item:1})中的1若改為-1則為降冪排列,否則為升冪排列。
>> node7_7_sort.js
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		mydb.collection("players").find().sort({name:1}).toArray((err,results)=>{
			if (err) 
				throw err;
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			for(i of results){
				res.write(`<span style="color:blue">${i.name}</span>:<span style="color:red">${i.occupation}</span><br>`);
			}
			res.end();
			db.close();
		});
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});

delete


刪除資料可使用deleteOne(query, callback)與deleltMany(query, callback)來刪除一或多筆資料。
>> node7_8_deleteff.js
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		mydb.collection("players").deleteOne({name:/M./},(err,obj)=>{
			if (err) 
				throw err;
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			res.write("One record is deleted.");
			res.end();
			db.close();
		});
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});

drop collection


>> node7_9_drop.js
const http = require('http');
const mongoClient = require('mongodb').MongoClient;
const url = "mongodb://localhost:27017/";

http.createServer(function(req, res){
	mongoClient.connect(url, function(err, db){
		if (err) 
			throw err;
		let mydb = db.db("mydb");
		mydb.collection("players").drop((err,done)=>{
			if (err) 
				throw err;
			res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
			if(done)
				res.write("Collection is deleted.");
			res.end();
			db.close();
		});
	});	
}).listen(3000, ()=>{
	console.log("Server is runing at port 3000.");
});

mongo shell


之前的操作也可以直接在mongo shell執行,在連線成功同時,再開啟一個DOS視窗,一樣到mongodb的安裝目錄執行mongo.ext(打mongo)。
  1. help: 查看指令
  2. show dbs: 查看資料庫
  3. use dbname: 換到dbname這個database,e.g. use mydb
  4. show collections: 查看collection列表
  5. db.collection.help(): 查看collection相關指令,e.g. db.players.help()
  6. db.collection.find(): 查看collection內資料,e.g. db.players.find()
  7. db.collection.insert(obj): 增加一筆資料,e.g. db.players.insert({name:"Tom", occupation:"Warrior"})
  8. db.collection.updateOne(query, newvalue): 更新一筆資料,e.g. db.players.updateOne({name:"Mary"}, {$set:{occupation:"Paladin"}})
  9. db.collection.deleteOne(query): 刪除一筆資料,e.g. db.players.deleteOne({name: "Tom"})
  10. db.collection.drop(): 刪除collection
除了使用help與db.collection.help()等指令來查詢可以之指令外,更多詳細內容亦可見官網