讀取靜態文件
MIME類型支持
緩存支持/控制
支持gzip壓縮
Range支持,斷點續傳
發佈爲可執行命令並能夠後臺運行,能夠經過npm install -g安裝
首先先構建好項目目錄,項目目錄以下:
html
project
|---bin 命令行實現放置腳本
|
|---public 靜態文件服務器默認靜態文件夾
|
|---src 實現功能的相關代碼
| |
| |__template 模板文件夾
| |
| |__app.js 主要功能文件(main文件)
| |__config.js 配置文件
|
|---package.josn (初始化)複製代碼
要啓動一個服務器,咱們須要知道這個服務器的啓動時的端口號,在config.js配置一下: node
let config = {
host:'localhost' //提示用 ,
port:8080 //服務器啓動時候的默認端口號,
path:path.resolve(__dirname,'..','test-dir') //靜態服務器啓動時默認的工做目錄
}
複製代碼
讀取靜態文件以前首先要先啓動服務器,以後全部的方法都在class Server方法裏數據庫
//handlebar 編譯模板,獲得一個渲染的方法,而後傳入實際數據數據就能夠獲得渲染後的HTML了
function list() {
let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'),'utf8');
return handlebars.compile(tmpl);//進行編譯,最後渲染
}class Server {
constructor(argv) {
this.list = list();
this.config = Object.assign({}, this.config, argv);
}
start() {
let server = http.createServer();//建立服務器
//當客戶端向服務端發出數據的時候,會出發request事件
server.on('request', this.request.bind(this));
server.listen(this.config.port, () => {//監聽端口號
let url = `http://${this.config.host}:${this.config.port}`;
debug(`server started at ${chalk.green(url)}`);
});
}
//發送錯誤信息,
sendError(err, req, res) {
res.statusCode = 500;
res.end(`${err.toString()}`);
}}
module.exports = Server;複製代碼
設計思路
首先輸入一個url時,可能對應服務器上的一個文件,或者對應一個目錄,
檢查是否文件仍是目錄
若是文件不存在,返回404狀態碼,發送not found頁面到客戶端
若是文件存在:
打開文件讀取
設置response header
發送文件到客戶端
若是是目錄就打開目錄列表
async request(req, res) {
//先取到客戶端想要的是文件或文件夾路徑
let { pathname } = url.parse(req.url);//獲取路徑的文件信息
let filepath = path.join(this.config.root, pathname);//服務器上的對應服務器物理路徑
try {
let statObj = await stat(filepath);//獲取路徑的文件信息
if (statObj.isDirectory()) {//若是是目錄的話,應該顯示目錄 下面的文件列表
let files = await readdir(filepath);//讀取文件的文件列表
files = files.map(file => ({//把每一個字符串變成對象
name: file,
url: path.join(pathname, file)
}));
//handlebar 編譯模板
let html = this.list({
title: pathname,
files
});
res.setHeader('Content-Type', 'text/html');設置請求頭
res.end(html);
} else {
this.sendFile(req, res, filepath, statObj);//讀取文件
}
} catch (e) {//不存在訪問內就發送錯誤信息
debug(inspect(e));//inspect把一個對象轉成字符
this.sendError(e, req, res);
}
}複製代碼
設計思路
緩存分爲強制緩存和對比緩存: npm
- 兩類緩存規則能夠同時存在,強制緩存優先級高於對比緩存,也就是說,當執行強制緩存的規則時,若是緩存生效,直接使用緩存,再也不執行對比緩存規則.
- 強制緩存若是生效,不須要再和服務器發生交互,而對比緩存無論是否生效,都須要與服務端發生交互
1. 第一次訪問服務器的時候,服務器返回資源和緩存的標識,客戶端則會把此資源緩存在本地的緩存數據庫中。2. 第二次客戶端須要此數據的時候,要取得緩存的標識,而後去問一下服務器個人資源是不是最新的。若是是最新的則直接使用緩存數據,若是不是最新的則服務器返回新的資源和緩存規則,客戶端根據緩存規則緩存新的數據。經過最後修改時間來判斷緩存是否可用
- Last-Modified:響應時告訴客戶端此資源的最後修改時間
- If-Modified-Since:當資源過時時(使用Cache-Control標識的max-age),發現資源具備Last-Modified聲明,則再次向服務器請求時帶上頭If-Modified-Since。
- 服務器收到請求後發現有頭If-Modified-Since則與被請求資源的最後修改時間進行比對。若最後修改時間較新,說明資源又被改動過,則響應最新的資源內容並返回200狀態碼;
- 若最後修改時間和If-Modified-Since同樣,說明資源沒有修改,則響應304表示未更新,告知瀏覽器繼續使用所保存的緩存文件。
ETag是資源標籤。若是資源沒有變化它就不會變。
json
- 客戶端想判斷緩存是否可用能夠先獲取緩存中文檔的ETag,而後經過If-None-Match發送請求給Web服務器詢問此緩存是否可用。
- 服務器收到請求,將服務器的中此文件的ETag,跟請求頭中的If-None-Match相比較,若是值是同樣的,說明緩存仍是最新的,Web服務器將發送304 Not Modified響應碼給客戶端表示緩存未修改過,可使用。
- 若是不同則Web服務器將發送該文檔的最新版本給瀏覽器客戶端
handleCache(req, res, filepath, statObj) {
let ifModifiedSince = req.headers['if-modified-since'];
let isNoneMatch = req.headers['is-none-match'];
res.setHeader('Cache-Control', 'private,max-age=30');//max-age=30緩存內容將在30秒後失效
res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toGMTString());
let etag = statObj.size;
let lastModified = statObj.ctime.toGMTString();
res.setHeader('ETag', etag);//獲取ETag
res.setHeader('Last-Modified', lastModified);//服務器文件的最後修改時間
//任何一個對比緩存頭不匹配,則不走緩存
if (isNoneMatch && isNoneMatch != etag) {//緩存過時
return fasle;
}
if (ifModifiedSince && ifModifiedSince != lastModified) {//緩存過時
return fasle;
}
//當請求中存在任何一個對比緩存頭,則返回304,不然不走緩存
if (isNoneMatch || ifModifiedSince) {//緩存有效
res.writeHead(304);
res.end();
return true;
} else {
return false;
}
}複製代碼
設計思路
瀏覽器都會攜帶本身支持的壓縮類型,最經常使用的兩種是gzip和deflate。根據請求頭Accept-Encoding,返回不一樣的壓縮格式.
getEncoding(req, res) {
let acceptEncoding = req.headers['accept-encoding'];//獲取客戶端發送的壓縮請求頭的信息
if (/\bgzip\b/.test(acceptEncoding)) {//若是是gzip的格式
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
} else if (/\bdeflate\b/.test(acceptEncoding)) {//若是是deflate的格式
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
} else {
return null;//不壓縮
}
}複製代碼
設計思路
- 該選項指定下載字節的範圍,常應用於分塊下載文件
- 服務器告訴客戶端可使用range response.setHeader('Accept-Ranges', 'bytes')
- Server經過請求頭中的Range: bytes=0-xxx來判斷是不是作Range請求,若是這個值存在並且有效,則只發回請求的那部分文件內容,響應的狀態碼變成206,若是無效,則返回416狀態碼,代表Request Range Not Satisfiable
getStream(req, res, filepath, statObj) {
let start = 0;//可讀流起始位置
let end = statObj.size - 1;//可讀流結束位置
let range = req.headers['range'];//獲取客戶端的range請求頭信息,
if (range) {//斷點續傳
res.setHeader('Accept-Range', 'bytes');
res.statusCode = 206;//返回整個內容的一塊
let result = range.match(/bytes=(\d*)-(\d*)/);//斷點續傳的分段內容不能有小數,網絡傳輸的最小單位爲一個字節
if (result) {
start = isNaN(result[1]) ? start : parseInt(result[1]);
end = isNaN(result[2]) ? end : parseInt(result[2]) - 1;
}
}
return fs.createReadStream(filepath, {
start, end
});
}複製代碼
#! /usr/bin/env node //這段代碼必定要寫在開頭,爲了兼容各個電腦平臺的差別性
// -d --root 靜態文件目錄 -o --host 主機 -p --port 端口號let yargs = require('yargs');
let Server = require('../src/app.js');
let argv = yargs.option('d',{
alias:'root',
demand:'false',
type:'string',
default:process.cwd(),
description:'靜態文件跟目錄' })
.option('o',{
alias:'host',
demand:'localhost',
type:'string',
description:'請配置監聽的主機'})
.option('p',{
alias:'root',
demand:'false',
type:'number',
default:8080,
description:'請配置端口號'})
.usage('http-static [options]').example(
'http-static -d / 8080 -o localhost','在本機的9090端口上監聽客戶端的請求'
).help('h').argv;
// argv = {d,root,o,host,p,port}let server = new Server(argv);//啓動服務server.start();
複製代碼
這樣命令行當中經過輸入http-static來直接啓動靜態文件服務器了,那麼命令行調用的功能也就實現了,最後用npm publish發佈一下,發佈到npm上面去了,咱們就能夠經過npm install -g來進行全局安裝了
瀏覽器