從0開始打造網頁伺服器
Tommy
延續上週直播主題
今天要用Node.js打造一個靜態網頁伺服器
什麼是靜態網頁伺服器?
不具備後端程式處理的
網頁伺服器
只單純提供html, css, js, jpg......等等這類靜態檔案給瀏覽器
還記得之前有直播過HTTP Header & Status code嗎?
今天終於要派上用場了!!!
什麼是伺服器?
所有的用戶都可以
連接到相同的電腦
伺服器都放在哪裡?
理想的地方
Title Text
實際的地方
感覺的地方
有沒有87%像
因為真的很冷啊
機房的特色
1. 很冷 => 要讓伺服器硬體降溫
2. 很吵 => 機器運轉風扇聲音很大
3. 很多綠色乖乖 => 一種有拜有保佑的概念
4. 24H運作 => 不然就不能半夜逛網站了
千萬不要放五香&巧克力口味
不要不信邪啊
傳統執行後端程式
都需要網頁伺服器
常見的網頁伺服器程式
- PHP => Apache
- ASP(.NET) => IIS
- JSP => Tomcat
- Nginx
- ......
那Node.js需要伺服器嗎?
Node.js本身自帶HTTP模組
可以自己啟動一個伺服器
Node.js和傳統伺服器有什麼不一樣?
摩斯 vs 麥當勞
使用HTTP模組前必須要懂
四大重點:Request, Response, IP, Port
request & response
IP & Port
127.0.0.1
指向自己電腦的IP位置
localhost也是指向自己電腦的IP
曾經因127.0.0.1鬧過笑話
剛入門後端時貼127.0.0.1的網址給別人
http預設80 Port
https預設443 Port
若不是監聽在預設port就必須自帶port編號
例如:http://192.168.0.1:3000
透過Node.js建立
最簡單的網頁伺服器
var http = require('http');
var server = http.createServer(function(request, response) {
response.write('hello world');
response.end();
});
server.listen(3000, '127.0.0.1');
一般執行Node.js程式
跑完就結束了
C:\>type demo.js
console.log('Hello World');
C:\>node demo
Hello World
C:\>_
(回到C:\>游標)
但HTTP啟動監聽後
程式不會停下來
因為若是關閉了,瀏覽器就無法和伺服器連線
所以必須要一直保持監聽狀態,等待瀏覽器連線
C:\>node web.js
(不會跳回C:\>游標)
沒有response.end()會怎樣
瀏覽器不知道什麼時候結束
每一個Port只能
由一支程式監聽
C:\>node web
events.js:183
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE :::3000
at Object._errnoException (util.js:1022:11)
at _exceptionWithHostPort (util.js:1044:20)
at Server.setupListenHandle [as _listen2] (net.js:1367:14)
at listenInCluster (net.js:1408:12)
at Server.listen (net.js:1492:7)
at Object.<anonymous> (C:\Users\Tommy\Desktop\demo\web.js:6:8)
at Module._compile (module.js:652:30)
at Object.Module._extensions..js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
回應Content-type
告訴瀏覽器傳送什麼內容
var http = require('http');
var server = http.createServer(function(request, response) {
response.writeHead(200, {
'Content-Type': 'text/html'
})
response.write('<h1>Hello World</h1>');
response.end();
});
server.listen(3000);
沒有Content-Type
瀏覽器會自動判斷
但有給Content-Type瀏覽器就會用這個類型解讀
試試看text/plain和image/png和application/zip
Status Code
任何回應都要有一個status code
沒問題就給200 OK
讀取index.html檔案回應
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
response.writeHead(200, {
'Content-Type': 'text/html'
})
var html = fs.readFileSync('./index.html')
response.write(html);
response.end();
});
server.listen(3000);
盡可能使用非同步操作
當大量request進來才不會被同步操作卡住
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
response.writeHead(200, {
'Content-Type': 'text/html'
})
fs.readFile('./index.html', function(err, html) {
if (!err) {
response.write(html);
response.end();
}
});
});
server.listen(3000);
同步 vs 非同步
同步會鎖住Event Loop
非同步會難閱讀程式碼
無論網址輸入什麼都是回應index.html網頁的內容
http://127.0.0.1:3000/index.css
http://127.0.0.1:3000/index.js
看到都是index.html見鬼了
解析要求路徑
/filename.html
/path/filename.txt
request.url
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
console.log(request.url)
response.writeHead(200, {
'Content-Type': 'text/html'
})
fs.readFile('./index.html', function(err, html) {
if (!err) {
response.write(html);
response.end();
}
});
});
server.listen(3000);
不過要注意一點
會包含GET參數
http://127.0.0.1:3000/index.css?v=1
如何排除GET參數
var url = require('url');
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
var pathname = url.parse(request.url).pathname;
console.log(pathname);
response.writeHead(200, {
'Content-Type': 'text/html'
})
fs.readFile('./index.html', function(err, html) {
if (!err) {
response.write(html);
response.end();
}
});
});
server.listen(3000);
取得檔案狀態fs.stat
var url = require('url');
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
var pathname = url.parse(request.url).pathname;
console.log(pathname);
response.writeHead(200, {
'Content-Type': 'text/html'
})
fs.stat('.' + pathname, function(err, stats) {
console.log(err, stats)
})
fs.readFile('./index.html', function(err, html) {
if (!err) {
response.write(html);
response.end();
}
});
});
server.listen(3000);
檔案不存在err會有錯誤物件
沒err錯誤物件還是有例外
也有可能是資料夾而不是檔案
stats.isFile()
檢查是不是檔案
var url = require('url');
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
var pathname = url.parse(request.url).pathname;
fs.stat('.' + pathname, function(err, stats) {
if(!err && stats.isFile()) {
fs.readFile('.' + pathname, function(err, html) {
if (!err) {
response.writeHead(200, {
'Content-Type': 'text/html'
})
response.write(html);
response.end();
}
});
} else {
response.writeHead(404);
response.write('Not Found');
response.end();
}
})
});
server.listen(3000);
Content-Type怪怪的
目前所有的Content-Type都固定是text/html
mime套件
記得要先npm install mime
mime.getType(filename)
var mime = require('mime');
var url = require('url');
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
var pathname = url.parse(request.url).pathname;
fs.stat('.' + pathname, function(err, stats) {
if(!err && stats.isFile()) {
fs.readFile('.' + pathname, function(err, html) {
if (!err) {
response.writeHead(200, {
'Content-Type': mime.getType(pathname)
})
response.write(html);
response.end();
}
});
} else {
response.writeHead(404);
response.write('Not Found');
response.end();
}
})
});
server.listen(3000);
預設index.html起始頁
http://127.0.0.1:3000/
var mime = require('mime');
var url = require('url');
var fs = require('fs');
var http = require('http');
var server = http.createServer(function(request, response) {
var pathname = url.parse(request.url).pathname;
if (pathname.endsWith('/')) {
pathname += '/index.html';
}
fs.stat('.' + pathname, function(err, stats) {
if(!err && stats.isFile()) {
fs.readFile('.' + pathname, function(err, html) {
if (!err) {
response.writeHead(200, {
'Content-Type': mime.getType(pathname)
})
response.write(html);
response.end();
}
});
} else {
response.writeHead(404);
response.write('Not Found');
response.end();
}
})
});
server.listen(3000);
http://127.0.0.1/demo
http://127.0.0.1/demo/
這兩個網址不一樣嗎?
demo是資料夾還是檔案
一般來說檔案至少會有個.
沒有.那我就可能是資料夾
※當然有例外(我們暫不考慮)
// ...(略)
} else {
if (!pathname.includes('.')) {
response.writeHead(302, {
'Location': pathname + '/' + (url.parse(request.url).search || "")
})
response.end();
return;
}
}
fs.stat('.' + pathname, function(err, stats) {
// ...(略)
如果考慮例外怎麼辦?
// 註解剛剛前面寫的部分
// ...(略)
fs.stat('.' + pathname, function(err, stats) {
if(!err && stats.isDirectory()) {
response.writeHead(302, {
'Location': pathname + '/' + (url.parse(request.url).search || "")
})
response.end();
return;
}
if(!err && stats.isFile()) {
// ...(略)
中文資料夾無法顯示
url傳送中文都會被編碼起來
ecsape()
encodeURI()
encodeURIComponent()
有何不同?
escape()
- 編碼成 %XX 和 %uXXXX
- ASCII字母、數字、@*/+不處理
- 用在非URL上
- 只能用unescape()解碼
encodeURI()
encodeURIComponent()
差別
- 都是編碼成 %XX
- encodeURI不處理ASCII字母、數字、符號
~!*()'@#$&=:/,;?+ - 使用decodeURI解碼
- encodeURIComponent不處理ASCII字母、數字、符號
~!*()' - 使用decodeURIComponent解碼
- URL多半建議用encodeURIComponent處理
//...(略)
}
var relativePathname = decodeURIComponent(pathname);
fs.stat('.' + relativePathname, function(err, stats) {
if(!err && stats.isDirectory()) {
response.writeHead(302, {
'Location': pathname + '/' + (url.parse(request.url).search || "")
})
response.end();
return;
}
if(!err && stats.isFile()) {
fs.readFile('.' + relativePathname, function(err, html) {
if (!err) {
response.writeHead(200, {
'Content-Type': mime.getType(relativePathname)
})
//...(略)
想把網站放在別的資料夾
透過啟動參數決定從哪個位置當作網站起點
// ...(略)
}
var relativePathname = decodeURIComponent((process.argv[2] || ".") + pathname);
fs.stat(relativePathname, function(err, stats) {
if(!err && stats.isDirectory()) {
response.writeHead(302, {
'Location': pathname + '/' + (url.parse(request.url).search || "")
})
response.end();
return;
}
if(!err && stats.isFile()) {
fs.readFile(relativePathname, function(err, html) {
if (!err) {
// ...(略)
快取機制
節省網路頻寬
有聽過SHA1嗎?
一種不可逆的加密演算法
相同內容才能算出相同結果
雖然現在聽說sha1已經被破解了
但我們拿來做快取的檢查OK的
crypto模組
記得要先require喔
// ...(略)
if (!err) {
var hash = crypto.createHash('sha1').update(html).digest('base64');
if (request.headers['if-none-match'] == hash) {
response.writeHead(304);
response.end();
return;
}
response.writeHead(200, {
'Content-Type': mime.getType(pathname),
"Etag": hash
})
// ...(略)
另外一種快取max-age
// ...(略)
}
var headers = {
'Content-Type': mime.getType(pathname),
"Etag": hash
};
if (headers["Content-Type"].startsWith('image/')) {
delete headers.Etag;
headers["Cache-Control"] = "max-age=3600";
}
response.writeHead(200, headers);
response.write(html);
// ...(略)
是不是把Header和Status Code用好用滿
我們只花了49行
打造出一個可客製化的靜態網頁伺服器
var mime = require('mime');
var url = require('url');
var fs = require('fs');
var http = require('http');
var crypto = require('crypto');
var server = http.createServer(function(request, response) {
var pathname = url.parse(request.url).pathname;
if (pathname.endsWith('/')) {
pathname += 'index.html';
}
var relativePathname = decodeURIComponent((process.argv[2] || ".") + pathname);
fs.stat(relativePathname, function(err, stats) {
if(!err && stats.isDirectory()) {
response.writeHead(302, {
'Location': pathname + '/' + (url.parse(request.url).search || "")
})
response.end();
return;
}
if(!err && stats.isFile()) {
fs.readFile(relativePathname, function(err, html) {
if (!err) {
var hash = crypto.createHash('sha1').update(html).digest('base64');
if (request.headers['if-none-match'] == hash) {
response.writeHead(304);
response.end();
return;
}
var headers = {
'Content-Type': mime.getType(pathname),
"Etag": hash
};
if (headers["Content-Type"].startsWith('image/')) {
delete headers.Etag;
headers["Cache-Control"] = "max-age=3600";
}
response.writeHead(200, headers);
response.write(html);
response.end();
}
});
} else {
response.writeHead(404);
response.write('Not Found');
response.end();
}
})
});
server.listen(3000);
這有什麼了不起
Apache和IIS都有的功能啊
你不能客製化Apache或IIS
但Node.js可以
需要的功能才寫進去,不需要的功能就不用寫進去
依照需求客製化更輕量化
最重要的是
了解HTTP的基礎原理
當你在用Apache和IIS有想過會丟出什麼header和status code嗎?
下週預告
介紹什麼是Git版本控制
適合完全沒使用過版本控制的人
Q & A
從0開始打造網頁伺服器
By Yi-Tai Lin
從0開始打造網頁伺服器
- 888