序言
手寫一個靜態服務可以對node中http模組有更深的理解,這是我們的初衷。http-server相信大家都用過,這裡我們要實現類似個功能。功能如下
- 啟動我們寫好的模組後,輸入localhost:3000開啟我們public目錄下的檔案(預設開啟index.html)
- 用到debug外掛,主要用於在命令列輸出一些日誌,我們只用基本的功能,所以沒有難點。不會戳這裡
- 可能用到chalk外掛,就是把命令列輸出的日誌五顏六色,變得好看,沒什麼太大作用,用法戳這裡
準備工作
我們的目錄解構如下
- 大家應該一看就懂啦,啟動我們服務,自動開啟public/index.html
- bin/www.js 是我們後面用命令列啟動服務的配置
- public是我們的靜態目錄
- app.js 主檔案
- config.js 配置檔案
- tmpl.html是我們用ejs編譯的模板,後面講到
先寫最簡單的config.js
let path = require('path');
let config = {
hostname:'127.0.0.1', //預設主機
port:3000, //預設埠
dir:path.join(__dirname,'','public') //預設開啟的目錄(絕對路徑)
};
module.exports = config;
複製程式碼
以上程式碼都能看得懂,下面開始寫我們主檔案
核心程式碼 app.js
1、引入所需的依賴包
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的模板檔案
複製程式碼
- mime解析檔案給你內容型別,用法
- ejs渲染引擎,我們用最簡單功能,不會也能看懂
2、http模組開啟服務
/*執行的條件 指定主機名
* 指定啟動的埠號
* 指定執行的目錄
*/
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方法
複製程式碼
截至到目前位置,簡單的服務已經開啟了,先來測試下效果吧
完美,控制檯列印出了內容,
3、實現handleRequest方法,即處理請求邏輯
列出我們要做什麼
- 解析url的路徑名
- 與預設配置中路徑(G://cgp-server/public)拼接
- 判斷是檔案還是資料夾還是404
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
//1、根據路徑 返回不同結果 如果是資料夾 顯示資料夾裡的內容
//2、如果是檔案 顯示檔案的內容
try{
let statObj=await stat(p);
}catch (e) {
//檔案不存在情況
this.sendError(req,res,e)
}
}
複製程式碼
try catch用於捕獲錯誤,當檔案不存在,呼叫sendError方法,先來實現這個錯誤的處理方法
4、檔案不存在的邏輯,sendError()
sendError(req,res,e){
debug(util.inspect(e)); //輸出錯誤,util模組提供方法
res.statusCode = 404;
res.end('Not Found');
}
複製程式碼
寫了這麼多了,測試下錯誤檔案能否列印錯誤
測試完美,此時我們應該判斷開啟的是檔案還是目錄,並給對應的方法,下面我們開始目錄的渲染方法
5、ejs渲染目錄列表
- 先宣告一個template模板,掛載到例項上
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //讀取ejs的模板檔案
class Server{
constructor(){
this.template = template //掛載到例項上
}
}
複製程式碼
- 如果是目錄,渲染出一個html頁展示目錄結構
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);
}
複製程式碼
- 我們來看下tmpl.html模板是怎麼寫的,ejs用法,我們只用最簡單的,所以應該能看懂
<!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>
複製程式碼
渲染目錄結構,我們已經寫完了,測試下看能不能執行
目前來看,無bug,接下來實現如果是檔案的話,直接把檔案內容渲染出來
6、檔案的渲染方法,即this.sendFile()方法
sendFile(req,res,p,statObj){
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8');
fs.createReadStream(p).pipe(res);//可讀流pipe到可寫流
}
複製程式碼
功能已經實現拉,不信我們測下
- 功能已經實現,但我們要求再增加三個功能
- 1、檢測是否支援快取
- 2、檢測是否支援壓縮
- 3、檢測是否支援範圍請求
6.1增加快取功能
- 修改下sendFile()方法新增三個功能
sendFile(req,res,p,statObj){
// 1、檢測是否有快取
if(this.cache(req,res,p,statObj)){ //如果有快取
res.statusCode = 304;
res.end();
return
}
//2、檢測是否支援壓縮
....
//3、檢測是否有範圍請求
....
}
複製程式碼
- cache()快取方法
快取有兩種方式,強制快取和協商快取
- 強制快取 服務端Catch-Control 、 Expires
- 協商快取 服務端Last-Modified 、Etag
- 協商快取 客戶端if-modified-since if-none-match 與服務端對應
- 貼下百度快取的解構給大家看下
看完這個圖,相信大家應該懂啦。下面開始寫快取方法
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; //否則走快取
}
複製程式碼
快取功能寫完了,我們測試下設定的頭有沒有新增上
快取我們就已經實現啦
6.2 實現壓縮功能
- node中zlib提供壓縮功能,這裡就不講怎麼用啦,用法戳官網
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()方法
sendFile(req,res,p,statObj){
// 1、檢測是否有快取
if(this.cache(req,res,p,statObj)){ //如果有快取
res.statusCode = 304;
res.end();
return
}
//2、檢測是否支援壓縮
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)
}
//3、檢測是否有範圍請求
....
}
複製程式碼
用1.txt檔案測試下
目前來看都還ok,還剩最後一個功能,實現範圍請求
6.3 實現範圍請求功能
- 客戶端傳送Range:bytes=0-3
- 服務端對應Accept-Range:bytes Content-Range:bytes 0-3/xxx Content-Length:xxx
由於可能同時會有壓縮和範圍請求,我們稍微改下前面的程式碼
sendFile(req,res,p,statObj){
// 1、檢測是否有快取
....
//2、檢測是否支援壓縮同時加上範圍請求
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()範圍請求的方法
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工具傳送請求,用法
- 1.txt的內容123456789。我們只想要前4個字元
測試完美,接下來我們還想實現輸入cgp-server ,自動開啟瀏覽器,開啟目錄。我們需要引用一個模組 yargs
7、yargs模組配置命令列的輸入
- yargs配置用法`
- 這裡我們只用最基本用法,一看就懂,詳細瞭解請看官網
7.1 npm link作用
- npm link命令可以將一個任意位置的npm包連結到全域性執行環境,從而在任意位置使用命令列都可以直接執行該npm包。
7.2 修改下package.json檔案
7.3 www.js的配置
7.3.1我們把app.js主檔案匯出給www.js使用
7.3.2修改www.js檔案
#! /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用法。然後我們輸入 cgp-server --help看效果
yargs.option('port',{ //yargs的基礎用法
alias: 'p', //別名
default: 3000, //預設值
description:'this is port', //描述
demand:false // 是否必須
})
複製程式碼
- 解釋下流程,先開啟服務,然後判斷系統,再然後根據不同的平臺執行自動開啟瀏覽器
測試下看能不能啟動
結尾
如果你能看到這裡,真的不容易,點個贊再走吧,原始碼分享給你