本文主要寫一個完整的靜態服務(命令列工具),http壓縮、快取以及範圍請求。
大致功能是:在當前目錄啟動一個服務,服務用來監聽請求,處理請求。並在瀏覽器上渲染出當前服務下的所有目錄
1.配置一個服務引數物件
let path = require('path');
let config = {
hostname:'localhost',
port:3000,
dir:path.join(__dirname,'..','public')
}
module.exports = config;
複製程式碼
2.寫出服務的基本框架
let config = require('./config');
let template = fs.readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8');
class Server {
constructor(args) {
this.config = config;// 將配置掛載在我們的例項上
this.template = template; //用於渲染當前服務下的所有目錄
}
//處理請求
handleRequest() {
}
start() {
let server = http.createServer(this.handleRequest.bind(this));
let { hostname, port } = this.config;
server.listen(port, hostname);
}
}
複製程式碼
這樣一個服務就可以開啟了,下面寫如何處理請求。 處理請求主要功能是:接收請求後,解析當前請求路徑下,判斷該路徑是否是目錄,如果是,則解析該目錄下所有子目錄,渲染到頁面上,如果不是,則直接渲染該檔案。為了方便處理,我們使用async+await實現路徑解析,程式碼如下:
async handleRequest(req, res) {
let { pathname } = url.parse(req.url, true);
let p = path.join(this.config.dir, pathname);
// 1.根據路徑 如果是資料夾 顯示資料夾裡的內容
// 2.如果是檔案 顯示檔案的內容
try { // 如果沒錯誤說明檔案存在
let statObj = await stat(p);
if (statObj.isDirectory()) {
// 現在需要一個當前目錄下的解析出的物件或者陣列
let dirs = await readdir(p);
dirs = dirs.map(dir => { // dirs就是要渲染的資料
return {
filename: dir,
pathname: path.join(pathname, dir)
}
});
let str = ejs.render(this.template, { dirs, title: 'ejs' });
res.setHeader('Content-Type', 'text/html;charset=utf8');
res.end(str);
} else {
// 檔案 傳送檔案
this.sendFile(req, res, p, statObj);
}
} catch (e) {
// 檔案不存在的情況
//錯誤
}
}
複製程式碼
其中用到的模版功能是將當前目錄下的子檔案,渲染到頁面上,程式碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%=title%></title>
</head>
<body>
<%dirs.map(item=>{%>
<li><a href="<%=item.pathname%>"><%=item.filename%></a></li>
<%})%>
</body>
</html>
複製程式碼
這樣一個基本的http服務就成形了,結下來我們實現sendFile功能,主要包括:快取、壓縮、範圍請求。 快取:主要包括強制快取和對比快取。程式碼如下:
cache(req, res, p, stat) {
// 實現快取
let since = req.headers['if-modified-since'];
let match = req.headers['if-none-match'];
let ssince = stat.ctime.toUTCString();//檔案修改時間
let smatch = stat.ctime.getTime() + stat.size;//檔案修改時間和檔案大小
res.setHeader('Last-Modified', ssince);
res.setHeader('ETag', smatch);
res.setHeader('Cache-Control','max-age=6');//強制快取只要它成立,就走快取
if (since != ssince) { // if-modified-since和last-modified
return false;
}
if (match != smatch) {
return false
}
return true;
}
複製程式碼
實現範圍請求:主要根據 請求頭Range,如Range:bytes=0-3表示請求前四個位元組,將響應發給瀏覽器時,需要設定響應頭
Accept-Ranges: bytes
Content-Length: 4
Content-Range: bytes 0-3/7877
設定狀態碼206
range(req, res, p, stat) {
let range = req.headers['range'];
if(range){
let [, start, end] = range.match(/(\d*)-(\d*)/) || [];
start = start ? parseInt(start) : 0;
end = end ? parseInt(end) : stat.size;
res.statusCode = 206;
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', end - start + 1);
res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
return { start, end };
}else{
return {start:0,end:stat.size}
}
}
複製程式碼
檔案壓縮功能:判斷請求頭中的Accept-Encoding是否有gzip,deflate等,有就壓縮,並設定響應頭Content-Encoding
// 實現服務端壓縮
gzip(req, res, p, stat) {
let header = req.headers['accept-encoding'];
if (header) {
if (header.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
fs.createReadStream(p).pipe(zlib.createGzip()).pipe(res);
} else if (header.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate');
fs.createReadStream(p).pipe(zlib.createDeflate()).pipe(res);
}
} else {
fs.createReadStream(p).pipe(res);
}
}
複製程式碼
然後綜合快取,壓縮,範圍請求,整理下sendFile方法
sendFile(req, res, p, stat) {
if (this.cache(req, res, p, stat)) {// 檢測是否有快取
res.statusCode = 304;
res.end();
return
};
this.compressAndRange(req, res, p, stat);
}
compressAndRange(req, res, p, stat) {
let compress = this.gzip(req, res, p, stat);
let { start, end } = this.range(req, res, p, stat); //範圍請求,返回開始位置和結束位置
if (compress) { // 返回的是一個壓縮流
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p, { start, end }).pipe(compress).pipe(res);
} else {
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
fs.createReadStream(p, { start, end }).pipe(res);
}
}
複製程式碼
這樣伺服器的功能都具備了,在實現一個命令列工具 在package.json中,新增:
"bin": {
"my-http-server": "bin/www.js"
}
複製程式碼
然後npm link
在bin/www.js 中啟動http server,這樣就可以通過my-http-server 命令啟動http伺服器.
寫www.js
#! /usr/bin/env node
let Server = require('../src/app');
new Server().start(); // 開啟服務
複製程式碼
這樣就可以通過my-http-server啟動服務,但是如果我想在任意目錄任意埠,任意域名下訪問呢。 使用yargs包,通過命令列引數控制
#! /usr/bin/env node
// 第一執行了命令後 會執行 bin/www.js
let yargs = require('yargs')
let argv = yargs.option('port', {//配置埠號
alias: 'p',
default: 3000,
demand: false,
description: 'this is port'
}).option('hostname', {//配置域名
alias: 'host',
default: 'localhost',
type: String,
demand: false,
description: 'this is hostname'
}).option('dir', {//配置目錄
alias: 'd',
default: process.cwd(),//命令的開啟目錄,任意目錄
type: String,
demand: false,
description: 'this is cwd'
}).usage('zf-http-server [options]').argv;
let Server = require('../src/app');
new Server(argv).start(); // 開啟服務
複製程式碼
注意這裡將引數argv傳入了Server類,然後需要修改下Server
class Server {
constructor(args) {
this.config = { ...config, ...args };// 將命令列中的引數傳給server
this.template = template;
}
...
...
}
複製程式碼
這樣我們通過:my-http-server -host localhost -p 3003 -d './public'命令就可以指定埠(3003),目錄(當前命令所在目錄/public),域名(localhost)啟動服務