首先來看看我要準備給你們寫的靜態文件服務器都要實現哪些功能,而後根據具體的功能咱們來一一的介紹html
支持如下功能node
下載地址git
啓動github
npm install // 安裝依賴
npm link // 建立鏈接
myselfserver // 在任意目錄下啓動服務
// 訪問 localhost:8080
複製代碼
其次咱們先大體看一下咱們的服務器的大體結構npm
接下來咱們來介紹咱們這個服務器json
要想實現一個服務器的功能,咱們每每須要設置一下服務器的主機名,端口號,靜態文件根目錄等信息,在這裏咱們的config.js幫咱們實現了這一配置,固然了這是基礎的配置,後面咱們還會講到如何經過命令行來更改配置。基礎配置以下:windows
let path = require('path');
let config = {
host: 'localhost',// 監聽主機
port: 8080,// 主機端口號
root: path.resolve(__dirname, '..')// 靜態文件根目錄
}
module.exports = config;
複製代碼
配置好以後接下來咱們就須要寫一個咱們的服務了,服務的代碼會在咱們的app.js中體現,其中包括咱們上面列舉的幾乎全部的功能,咱們先來看一下app.js的代碼結構瀏覽器
const fs = require('fs');
const url = require('url');
const http = require('http');
const path = require('path');
const config = require('./config');
class Server {
constructor(argv) {
// 生產handlebars模板
this.list = list()
// 處理命令行中設置的參數,來重寫基本參數
this.config = Object.assign({}, config, argv)
}
start() {
/*啓動http服務*/
let server = http.createServer();
server.on('request', this.request.bind(this));
let url = `http://${config.host}:${config.port}`;
server.listen(this.config.port, ()=>{
// (1)支持輸出日誌debug 在下文中會講一下使用它的注意事項
debug(`server started at ${chalk.green(url)}`);
})
}
/*(2)讀取靜態文件*/
async request(req,res) {
// 由於響應可能出錯,因此在咱們的代碼中這裏會用try catch包一下。
// try
// 文件夾: (2.1)模板引擎渲染展現文件列表 這裏用handlebars模板引擎來渲染
// 文件:-->sendFile
// catch
// 處理錯誤交給-->sendError
}
/* send file to browser*/
sendFile (req, res, filePath, statObj) {
// (2.2)處理文件併發送給瀏覽器 添加MIME類型支持
}
/* handle error*/
sendError (error, req, res) {
// 公用的的錯誤處理函數
}
/*cache 是否走緩存*/
isCache (req, res, filePath, statObj) {
// (3)處理緩存
}
/*broken-point continuingly-transferring 斷點續傳*/
rangeTransfer (req, res, filePath, statObj) {
// (4)支持斷點續傳
}
/*compression 壓縮*/
compression (req, res) {
// (5)支持gzip和deflate壓縮
}
}
module.exports = Server;
(6)myselfsever -d指定靜態文件的根目錄 -p指定端口號 -o指定監聽的主機 這個命令實現的腳本代碼在 bin目錄下的commond文件中
複製代碼
注:每用到一個模塊的時候記得提早安裝到本地緩存
const debug = require('debug'); // 安裝引入debug包
debug('staticserver:app');
// debug 第三方模塊返回的是一個函數,該函數執行須要傳入兩個參數,一個是你當前項目的名稱,即package.json 中的name值,第二個參數是你的模塊的服務入口文件的名稱。在我們的這個項目中是app.js
複製代碼
當咱們項目中要使用debug錯誤輸出模塊時,須要配置環境變量咱們的debug日誌纔會被輸出bash
環境變量的配置方法 windows上配置環境變量
$ set DEBUG=staticserver:app
複製代碼
$ set DEBUG=staticserver:*
複製代碼
mac上配置環境變量
$ export DEBUG=staticserver:app
$ export DEBUG=staticserver:*
複製代碼
若是是文件夾的話顯示文件列表,這裏咱們用到了handlebars模板引擎
const handlebars = require('handlebars'); // 模版引擎
// 調用handlebars.compile方法生成一個模板
function list () {
let tmpl = fs.readFileSync(path.resolve(__dirname,'template','list.html'),'utf8');
return handlebars.compile(tmpl)
}
複製代碼
接下來咱們來看訪問的路徑是文件的狀況。這時候咱們會根據文件的後綴的不一樣返回不一樣的響應類型,這時咱們就用到了咱們的mime模塊
/*靜態文件服務器*/
const { promisify, inspect } = require('util');
const mime = require('mime');
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
async request(req,res) {
// 先取到客戶端想訪問的路徑
let { pathname } = url.parse(req.url);
// 若是訪問的是favicon.ico 的話返回一個錯誤信息
if (pathname == '/favicon.ico') {
return this.sendError('not favicon.ico',req,res)
}
// 獲取訪問的文件或文件夾的路徑
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)
}))
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)); // 把一個對象轉化爲字符串,由於有的tosting會生成object object
this.sendError(e, req, res);
}
}
// 接下來咱們來看訪問的路徑是文件的狀況。這時候咱們會根據文件的後綴的不一樣返回不一樣的響應類型,這時咱們就用到了咱們的mime模塊
sendFile (req, res, filePath, statObj) {
// 若是緩存存在的話走緩存
if(this.isCache(req, res, filePath, statObj)){
return;
}
res.statusCode = 200; // 能夠省略
res.setHeader('Content-Type', mime.getType(filePath) + ';charset=utf-8');
let encoding = this.compression(req,res);
// 是否是須要壓縮
if(encoding) {
// 在這裏使用斷點續傳
this.rangeTransfer(req, res, filePath, statObj).pipe(encoding).pipe(res)
} else {
this.rangeTransfer(req, res, filePath, statObj).pipe(res)
}
}
複製代碼
要想理解下面緩存是否存在,緩存是否有效來判斷要不要走緩存,你們能夠先看一下這篇文章node中的緩存機制能夠加深對緩存的理解
/*cache 是否走緩存*/
isCache (req, res, filePath, statObj) {
let ifNoneMatch = req.headers['if-none-match'];
let ifModifiedSince = req.headers['if-modified-since'];
res.setHeader('Cache-Control','private,max-age=10');
res.setHeader('Expires',new Date(Date.now() + 10*1000).toGMTString);
let etag = statObj.size;
let lastModified = statObj.ctime.toGMTString();
res.setHeader('Etag',etag)
res.setHeader('Last-Modified',lastModified);
if(ifNoneMatch && ifNoneMatch != etag) {
return false
}
if(ifModifiedSince && ifModifiedSince != lastModified){
return false
}
if(ifNoneMatch || ifModifiedSince) {
res.writeHead(304);
res.end();
return true
} else {
return false
}
}
複製代碼
該選項指定下載字節的範圍,常應用於分塊下載文件
range的表示方式有多種,如100-500,則指定從100開始的400個字節數據;-500表示最後的500個字節;5000-表示從第5000個字節開始的全部字節
另外還能夠同時指定多個字節塊,中間用","分開
服務器告訴客戶端可使用range response.setHeader('Accept-Ranges', 'bytes')
Server經過請求頭中的Range:bytes=0-xxx來判斷是不是作Range請求,若是這個值存在並且有效,則只發回請求的那部分文件內容,響應的狀態碼變成206,若是無效,則返回416狀態碼,代表Request Range Not Satisfiable
/*broken-point continuingly-transferring 斷點續傳*/
rangeTransfer (req, res, filePath, statObj) {
let start = 0;
let end = statObj.size-1;
let range = req.headers['range'];
if(range){
res.setHeader('Accept-range','bytes');
res.statusCode=206// 返回整個內容的一塊
let result = range.match(/bytes=(\d*)-(\d*)/);
start = isNaN(result[1]) ? start : parseInt(result[1]);
end = isNaN(result[2]) ? end : parseInt(result[2]) - 1
}
return fs.createReadStream(filePath, {
start,
end
})
}
複製代碼
服務器會根據客戶端請求中的req.headers['accept-encoding']這個字段中的值來判斷客戶端支持哪一種解壓類型來進行壓縮的
/*compression 壓縮*/
compression (req, res) {
let acceptEncoding = req.headers['accept-encoding'];//163.com
if(acceptEncoding) {
if(/\bgzip\b/.test(acceptEncoding)){
res.setHeader('Content-Encoding','gzip');
return zlib.createGzip();
} else if(/\bdeflate\b/.test(acceptEncoding)) {
res.setHeader('Content-Encoding','deflate');
return zlib.createDeflate();
} else {
return null
}
}
}
複製代碼
這個功能是在咱們的bin目錄下面的command 文件中實現的。 文件開頭的#! /usr/bin/env node 這句命令是告訴該腳本用哪一種程序來執行,詳細可看這邊文章Node.js 命令行程序開發教程
#! /usr/bin/env node
let yargs = require('yargs');
let Server = require('../src/app.js')
let argv = yargs.option('d', {
alias: 'root',
demand: false,
description: '請配置監聽靜態文件目錄',
default: process.cwd(),
type: 'string'
}).option('o', {
alias: 'host',
demand: false,
description: '請配置監聽的主機',
default: 'localhost',
type: 'string'
}).option('p', {
alias: 'port',
demand: false,
description: '請配置監聽的主機的端口號',
default: 8080,
type: 'number'
}).usage('myselfsever -d / -p 8080 -o localhost')
.example(
'myselfsever -d / -p 9090 -o localhost', '在本機器的9090端口上監聽客戶端發來的請求'
).help('h').argv;
let server = new Server();
server.start(argv);
複製代碼
天天都進步一點;不要畏懼陌生的事物,天天都學習一點;