node 靜態資源伺服器
用node實現一個靜態資源伺服器,讀取一個目錄下的檔案,如果是目錄,顯示該目錄下的檔名。支援命令設定埠和靜態目錄,支援防盜鏈、快取等功能。
建立目錄 staticServer
- npm init 在package.json里加入如下程式碼。bin裡面是相應的命令,執行 static-server,即執行bin下面的www.js檔案
"bin": {
"static-server": "./bin/www.js"
}
複製程式碼
config
- 在config目錄下建立index.js,配置檔案,root是靜態目錄,port是埠號。
module.exports = {
root:'public',
port:8080
};
複製程式碼
bin目錄
- 在bin目錄下建立www.js
- yargs是一個獲取命令列引數的庫
#! /usr/bin/env node
const argv = require('yargs')
.usage('static-server')
.options('port', {
alias: 'p',
describe: '設定埠號',
default: 8080
})
.options('root', {
alias: 'r',
describe: '設定靜態目錄',
default: process.cwd()
})
.help()
.argv;
const config = Object.assign(require('../config'),argv);
let StaticServer = require('../src');
let server = new StaticServer(config);
複製程式碼
src
- src是原始碼,template裡是模板檔案,用來展示讀取的目錄資訊,index是node服務的檔案
- 要用到獲取檔案資訊、讀取檔案和目錄,把非同步的方便變成同步的寫法,用到了util.promisify
const fsStat = util.promisify(fs.stat);
const fsReadFile = util.promisify(fs.readFile);
const fsReaddir = util.promisify(fs.readdir);
複製程式碼
目錄模版
- 讀取模版檔案,用Handlebars做模版引擎
let template;
function getTemplate() {
fs.readFile(path.join(__dirname, 'template', 'template.html'), 'utf8', (err, html) => {
if (err) {
console.log(err);
} else {
template = Handlebars.compile(html);
}
});
}
複製程式碼
StaticServer類
- 建立staticServer類,cacheType是快取的型別,cwd是工作目錄
class StaticServer {
constructor(config) {
this.cacheType = ['CacheControl', 'Expires', 'LastModified', 'ETag'];
this.port = config.port;
this.root = config.root;
this.cwd = process.cwd();
this.createServer();
}
}
複製程式碼
createServer啟動服務
createServer() {
try {
const server = http.createServer(this.requestListener.bind(this));
server.listen(this.port, () => {
console.log(`server is ok;http://localhost:${this.port}`);
});
} catch (e) {
this.errorListener(e);
}
}
複製程式碼
errorListener
- errorListener容錯處理函式
errorListener(err) {
console.log(err);
}
複製程式碼
requestListener
- createServer監聽函式,req是請求、res是相應、dir是檔案目錄。
- 用mime模組獲得mimetype,設定相應頭 Content-Type,如果是文字,設定charset是utf-8。
- fsStat獲取檔案的資訊。如果是目錄,獲取目錄;如果是檔案,返回檔案。
- 如果是圖片,先設定防盜鏈,返回檔案。
async requestListener(req, res) {
const pathname = url.parse(req.url).pathname;
const dir = path.join(this.root, pathname);
try {
let contentType = mime.getType(dir);
if (contentType&&contentType.match('text')) {
contentType += ';charset=utf-8';
}
res.setHeader('Content-Type', contentType);
const stat = await fsStat(dir);
if (stat.isDirectory()) {
this.getDir(req,res,pathname,dir);
} else {
this.proxyGetFile(req,res,pathname,dir,stat);
}
} catch (e) {
res.statusCode = 404;
res.end(e.toString());
this.errorListener(e);
}
}
複製程式碼
讀取目錄
- 迴圈目錄下的檔名,得到一個陣列,每個元素是一個物件,名字和url。
- 返回模板
async getDir(req,res,pathname,dir) {
const dirs = await fsReaddir(dir);
const list = dirs.map(dir => ({name: dir, url: path.join(pathname, dir)}));
const html = template({
title: dir,
list
});
res.end(html);
}
複製程式碼
防盜鏈
- 如果是當前伺服器訪問的就返回該圖片,如果是其他伺服器訪問,返回空白圖片
sendForbidden(req,res,pathname,dir,stat) {
const referer = req.headers['referer'] || req.headers['refer'];
if (referer && url.parse(referer).host !== req.headers['host']) {
console.log('防盜鏈');
res.statusCode = 403;
fs.createReadStream(path.join(__dirname, 'forbidden.png')).pipe(res);
return false;
}
return true;
}
複製程式碼
讀取檔案
- 設定快取,該類有getFileCacheControl、getFileExpires、getFileLastModified、getFileETag方法
proxyGetFile(req,res,pathname,dir,stat) {
if (mime.getType(dir).match('image')) {
//圖片
if (!this.sendForbidden(req,res,pathname,dir,stat)) {
return;
}
}
for (let i = 0; i < this.cacheType.length; i++) {
if (this[`getFile${this.cacheType[i]}`](req,res,pathname,dir,stat)) {
console.log(this.cacheType[i]);
return;
}
}
this.getFile(req,res,pathname,dir,stat);
}
複製程式碼
強制快取
- 通過設定響應頭Expires和Cache-Control來實現強制快取
getFileExpires(req,res,pathname,dir,stat) {
res.setHeader('Expires', new Date(Date.now() + 60 * 1000));
}
getFileCacheControl(req,res,pathname,dir,stat) {
res.setHeader('Cache-Control', 'max-age=60');
}
複製程式碼
協商快取
- 設定響應頭的Last-Modified為檔案修改時間,如果請求頭的If-Modified-Since和檔案修改時間一樣,設定Status Code為304,讓客戶端從快取裡獲取資料。
- 設定響應頭的ETag為檔案修改時間的md5值,如果請求頭的If-None-Match和md5值一樣,設定Status Code為304,讓客戶端從快取裡獲取資料。
getFileLastModified(req,res,pathname,dir,stat) {
const lastModified = stat.ctime.toGMTString();
res.setHeader('Last-Modified', lastModified);
if (req.headers['if-modified-since'] === lastModified) {
res.statusCode = 304;
res.end();
return true;
}
}
getFileETag(req,res,pathname,dir,stat) {
let ifNoneMatch = req.headers['if-none-match'];
let etag = crypto.createHash('md5').update(stat.ctime.toGMTString(), 'utf8').digest('hex');
res.setHeader('ETag', etag);
if (ifNoneMatch == etag) {
res.statusCode = 304;
res.end();
return true;
}
}
複製程式碼
壓縮
- 根據請求頭Accept-Encoding,返回不同的壓縮格式
compressFile(req,res,inp) {
const acceptEncodings = req.headers['accept-encoding'];
if(/\bgzip\b/.test(acceptEncodings)){
this.gzipFile(req,res,inp);
return true;
}else if(/\bdeflate\b/.test(acceptEncodings)){
this.deflateFile(req,res,inp);
return true;
}
return false;
}
gzipFile(req,res,inp) {
const gzip = zlib.createGzip();
res.setHeader('Content-Encoding','gzip');
inp.pipe(gzip).pipe(res);
}
deflateFile(req,res,inp) {
const deflate = zlib.createDeflate();
res.setHeader('Content-Encoding','deflate');
inp.pipe(deflate).pipe(res);
}
複製程式碼
讀取檔案
- 先進行壓縮
getFile(req,res,pathname,dir,stat) {
const fsReadStream = fs.createReadStream(dir);
if(!this.compressFile(req,res,fsReadStream))
fsReadStream.pipe(res);
}
複製程式碼
總結
根據近段時間學習,做了靜態資源伺服器,根據請求頭做一些操作,返回響應的響應頭。功能有待改善,後續會繼續更新,併發不到npm上。