手寫一個靜態服務能夠對node中http模塊有更深的理解,這是咱們的初衷。http-server相信你們都用過,這裏咱們要實現相似個功能。功能以下css
let path = require('path');
let config = {
hostname:'127.0.0.1', //默認主機
port:3000, //默認端口
dir:path.join(__dirname,'','public') //默認打開的目錄(絕對路徑)
};
module.exports = config;
複製代碼
以上代碼都能看得懂,下面開始寫咱們主文件html
let http = require('http');
let url = require('url');
let path = require('path');
let util = require('util');
let fs = require('fs');
let zlib = require('zlib');
let mime = require('mime'); // 獲得內容類型
let debug = require('debug')('*'); // 打印輸出 會根據環境變量控制輸出
let chalk = require('chalk'); // 粉筆
let ejs = require('ejs'); // 模板引擎
//先聲明好,下面解釋
let config = require('./config');
let stat = util.promisify(fs.stat);//promise化 fs.stat方法
let readdir = util.promisify(fs.readdir);
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //讀取ejs的模板文件
複製代碼
/*運行的條件 指定主機名
* 指定啓動的端口號
* 指定運行的目錄
*/
let config = require('./config'); //引入配置文件
class Server { //聲明類
constructor() {
this.config = config; //講配置掛載再咱們的實例上
}
handleRequest(req,res){ //確保這裏的this都是實例
}
start(){//服務開始的方法
let server =http.createServer(this.handleRequest.bind(this));
let {hostname,port} = this.config; //解構主機名和端口
server.listen(port,hostname);
debug(`http://${hostname}:${port} start`) //命令行中打印
}
}
//開啓一個服務
let server = new Server();
server.start(); //調用start方法
複製代碼
截至到目前位置,簡單的服務已經開啓了,先來測試下效果吧node
完美,控制檯打印出了內容,git
列出咱們要作什麼github
let stat = util.promisify(fs.stat);//promise化 fs.stat方法
async handleRequest(req,res){ //確保這裏的this都是實例
let {pathname} = url.parse(req.url,true); //獲取url的路徑
let p = path.join(this.config.dir,pathname); // 多是G:/cgp-server/public 多是G://cgp-server/public/index.html
//一、根據路徑 返回不一樣結果 若是是文件夾 顯示文件夾裏的內容
//二、若是是文件 顯示文件的內容
try{
let statObj=await stat(p);
}catch (e) {
//文件不存在狀況
this.sendError(req,res,e)
}
}
複製代碼
try catch用於捕獲錯誤,當文件不存在,調用sendError方法,先來實現這個錯誤的處理方法npm
sendError(req,res,e){
debug(util.inspect(e)); //輸出錯誤,util模塊提供方法
res.statusCode = 404;
res.end('Not Found');
}
複製代碼
寫了這麼多了,測試下錯誤文件可否打印錯誤 json
![]()
測試完美,此時咱們應該判斷打開的是文件仍是目錄,並給對應的方法,下面咱們開始目錄的渲染方法api
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //讀取ejs的模板文件
class Server{
constructor(){
this.template = template //掛載到實例上
}
}
複製代碼
if(statObj.isDirectory()){
//若是是目錄 列出目錄內容能夠點擊
let dirs = await readdir(p); //public下面的目錄結構=>[index.html,style.css]
dirs =dirs.map(dir=>{
return {
filename:dir,
path:path.join(pathname,dir)
}
});
//dirs就是要渲染的數據
//格式以下[{filename:index.html,path:'/index.html'},{{filename:style.css,path:''/style.css}}]
let str =ejs.render(this.template,{dirs}); //ejs渲染方法
// console.log(str);
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end(str);
}
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
//循環dirs中的內容到頁面中
<% dirs.map(item=>{%>
<li><a href="<%=item.path%>"><%=item.filename%></a></li>
<%})%>
</body>
</html>
複製代碼
渲染目錄結構,咱們已經寫完了,測試下看能不能運行promise
目前來看,無bug,接下來實現若是是文件的話,直接把文件內容渲染出來瀏覽器
sendFile(req,res,p,statObj){
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8');
fs.createReadStream(p).pipe(res);//可讀流pipe到可寫流
}
複製代碼
功能已經實現拉,不信咱們測下
sendFile(req,res,p,statObj){
// 一、檢測是否有緩存
if(this.cache(req,res,p,statObj)){ //若是有緩存
res.statusCode = 304;
res.end();
return
}
//二、檢測是否支持壓縮
....
//三、檢測是否有範圍請求
....
}
複製代碼
緩存有兩種方式,強制緩存和協商緩存
看完這個圖,相信你們應該懂啦。下面開始寫緩存方法
cache(req,res,p,statObj){ //實現緩存
/* 強制緩存 服務端 Cache-Control Expires
協商緩存 服務端 Last-Modified Etag
協商緩存 客戶端 if-modified-since if-none-match
etag ctime + 文件的大小
Last-modified ctime
強制緩存
*/
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString());//10秒後從新發請求
let etag = statObj.ctime.toGMTString() + statObj.size; //文件修改時間和文件大小
let lastModified = statObj.ctime.toGMTString(); //文件的修改時間
res.setHeader('Etag', etag);
res.setHeader('Last-Modified', lastModified);
let ifNoneMatch = req.headers['if-none-match'];
let ifModifiedSince = req.headers['if-modified-since'];
if (etag != ifNoneMatch) { //不相等,不走緩存
return false;
}
if (lastModified != ifModifiedSince) { //同理
return false;
}
return true; //不然走緩存
}
複製代碼
緩存功能寫完了,咱們測試下設置的頭有沒有添加上
緩存咱們就已經實現啦
gzip(req,res,p,statObj){
// 客戶端 Accept-Encoding: gzip, deflate, br
// 服務端 Content-Encoding: gzip
let encoding = req.headers['accept-encoding']; //獲取請求頭的接收的壓縮格式
if (encoding) {
if (encoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip')
return zlib.createGzip();//返回一個gzip的壓縮流
} else if (encoding.match(/\bdeflate\b/)) {
res.setHeader('content-encoding', 'deflate');
return zlib.createDeflate(); //返回createDeflate的壓縮流
} else {
return false; //不然不支持壓縮
}
} else {
return false;//不然不支持壓縮
}
}
複製代碼
sendFile(req,res,p,statObj){
// 一、檢測是否有緩存
if(this.cache(req,res,p,statObj)){ //若是有緩存
res.statusCode = 304;
res.end();
return
}
//二、檢測是否支持壓縮
res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
let compress =this.gzip(req,res,p,statObj);
if(compress){ //檢測是否壓縮。返回的是壓縮流
return fs.createReadStream(p).pipe(compress).pipe(res);
}else{ //不支持壓縮直接把文件讀出來便可
return fs.createReadStream(p).pipe(res)
}
//三、檢測是否有範圍請求
....
}
複製代碼
用1.txt文件測試下
目前來看都還ok,還剩最後一個功能,實現範圍請求
因爲可能同時會有壓縮和範圍請求,咱們稍微改下前面的代碼
sendFile(req,res,p,statObj){
// 一、檢測是否有緩存
....
//二、檢測是否支持壓縮同時加上範圍請求
res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
let compress =this.gzip(req,res,p,statObj);
let {start,end} = this.range(req,res,p,statObj); //解構開始和結束的位置
if(compress){ //檢測是否壓縮。返回的是壓縮流
return fs.createReadStream(p,{start,end}).pipe(compress).pipe(res);
}else{
// res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
return fs.createReadStream(p,{start,end}).pipe(res)
}
}
複製代碼
range(req, res, statObj, p) {
//客戶端 Range:bytes=0-3
//服務端 Accept-Range:bytes Content-Range:bytes 0-3/8777
let range = req.headers['range']; //若是有範圍請求
if (range) {
let [, start, end] = range.match(/(\d*)-(\d*)/); //解構出開始和結束的位置
start = start ? Number(start) : 0; //start設置默認值
end = end ? Number(end) : statObj.size - 1; //end設置默認值
res.statusCode = 206; //狀態碼 206範圍請求
res.setHeader('Accept-Ranges',"bytes");
res.setHeader('Content-Length',end-start+1);
res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`);
return {start,end};
}else {
return {start:0, end:statObj.size};
}
}
複製代碼
基本功能已經實現,測試下代碼,咱們用curl工具發送請求,用法
測試完美,接下來咱們還想實現輸入cgp-server ,自動開啓瀏覽器,打開目錄。咱們須要引用一個模塊 yargs
#! /usr/bin/env node //執行命令後會執行 bin/www.js
const yargs = require('yargs');
let argv = yargs.option('port',{ //yargs的基礎用法
alias: 'p', //別名
default: 3000, //默認值
description:'this is port', //描述
demand:false // 是否必須
}).option('hostname',{
alias: 'h',
default: 'localhost',
description:'this is hostname',
demand:false
}).option('dir',{
alias: 'd',
default: process.cwd(),
description:'this is cwd',
demand:false
}).usage('cgp-server [options]' ).argv;
//開啓服務
let Server = require('../src/app.js');
new Server(argv).start();
// 判斷是win仍是mac平臺
let platform = require('os').platform();
//開啓子進程
let {exec} = require('child_process');
//win系統 win32
if(platform==="win32"){
exec(`start http://${argv.hostname}:${argv.port}`)
}else {
exec(`open http://${argv.hostname}:${argv.port}`)
}
複製代碼
yargs.option('port',{ //yargs的基礎用法
alias: 'p', //別名
default: 3000, //默認值
description:'this is port', //描述
demand:false // 是否必須
})
複製代碼
測試下看能不能啓動
若是你能看到這裏,真的不容易,點個贊再走吧,源碼分享給你