用node模擬一個簡單的靜態伺服器
我們都知道在本地起個服務直接一個
http-server -p 3000
一個埠為3000的服務就起來了,我們可以直接在瀏覽器訪問3000埠,就能拿到我們需要的頁面,那麼如果想自己實現一個這樣的工具怎麼做呢?不要急,看我慢慢分析寫出來
寫之前我們先要搞清楚要做什麼:用自己寫的包,起一個服務,訪問3000埠回車,應該顯示出public下的目錄列表,後面加/index.html,就應該顯示index.html的內容來
- 首先先init一個專案,並下載一些包,mime(解析返回頭型別),chalk(五顏六色的輸出),debug
- 建立自己的目錄結構:
index.css
body{
background : red
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head >
<body>
我很美
<link rel="stylesheet" href="/index.css">
</body>
</html>
config.js
let path = require('path')
//啟動服務的配置項
let config = {
hostname:'localhost',
port:3000,
dir:path.join(__dirname,'..','public')
}
module.exports = config
app.js
這裡用到了debug(用法請看這裡)
// set DEBUG=static:app (win32
// export DEBUG=static:app (ios
let config = require('./config')
let path = require('path')
let fs = require('fs')
let mime = require('mime')
let chalk = require('chalk')
let util = require('util')
let url = require('url')
let http = require('http')
let stat = util.promisify(fs.stat)
//debug 可以後面放參數,可以根據後面的引數決定是否列印
let debug = require('debug')('static:app')
//console.log(chalk.green('hello'));
//debug('app')
class Server { //首先寫一個Server類
constructor(){
this.config = config
}
handleRequest(){
return (req,res)=>{
}
}
start(){ //例項上的start方法
let {port,hostname} = this.config
let server = http.createServer(this.handleRequest())
//用http啟動一個服務,回撥裡執行handleRequest方法
let url = `http://${hostname}:${chalk.green(port)}`
debug(url);
server.listen(port, hostname);
}
}
let server = new Server()
server.start()
node執行app.js,(在執行之前要先執行set DEBUG=static:app)得到下圖
如果你想實時監控專案的變化可以安裝一個supervisor(npm install supervisor -g),直接執行supervisor app.js就能監控了,不過不是很穩定······
這個時候可以假設訪問的是http://localhost:3000/index.html,是個檔案,我們就可以寫handleRequest方法了
handleRequest(){
return async(req,res)=>{
//處理路徑
let {pathname} = url.parse(req.url,true)
//因為拿到的pathname會是/index,這樣會直接指向c盤,加./的話就變成當前
let p = path.join(this.config.dir,'.'+pathname)
try{
let statObj = await stat(p)//判斷p路徑對不對
if(statObj.isDirectory()){
}else{
//是檔案就直接讀了
res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
fs.createReadStream(p).pipe(res)
}
}catch(e){
res.statusCode = 404;
res.end()
}
}
}
- 架子搭出來了,那麼就開始寫吧,因為報錯和展示頁面資訊要重複利用,所以把他們單獨提出來封成兩個方法
sendFile(req,res,p){
//是檔案就直接讀了
res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
fs.createReadStream(p).pipe(res)
}
sendError(req,res,e){
debug(util.inspect(e).toString())
res.statusCode = 404;
res.end()
}
- 假如訪問的是個目錄的話,我們應該把目錄展示出來,這樣的話最好使用模板引擎,常見的模板引擎有:handlebar ejs
這我們用的是ejs,用法 render(‘檔案內容’,‘變數引數’),裝一下 :npm install ejs
src/tmpl.ejs
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>staticServer</title>
</head>
<body>
<!-- 碰到js就 <% %>包起來,求值用= -->
<%dirs.forEach(dir=>{%>
<li><a href="<%=dir.path%>"><%=dir.name%></a></li>
<% })%>
</body>
</html>
那在app.js中就要放進去
let ejs = require('ejs')
let tmpl = fs.readFileSync(path.join(__dirname,'tmpl.ejs'),'utf8')
let readDir = util.promisify(fs.readdir)//讀取目錄用的方法
再把tmpl掛在this上
app.js那麼如果是目錄的話這個程式碼就這麼寫
if(statObj.isDirectory()){
//如果是目錄的話就應該把目錄放出去
//用模板引擎寫 handlebal ejs underscore jade
let dirs = await readDir(p)
debug(dirs)//返回的是個陣列[index.css,index.html]
dirs = dirs.map(dir => ({
path: path.join(pathname, dir),
name: dir
}))
let content = ejs.render(this.tmpl,{dirs})
res.setHeader('Content-Type','text/html;charset=utf8')
res.end(content)
}else{
this,this.sendFile(req,res,p)
}
下面就是細化的問題了,總共3個方向:
- 如果檔案訪問過,就應該有快取的功能,
- 檔案很大應該有壓縮,
- 範圍請求
快取
cache(req,res,statObj){
//etag if-none-match
//Last-Modified if-modified-since
//Cache-Control
//ifNoneMatch一般是內容的md5戳 => ctime+size
let ifNoneMatch = req.headers['if-none-match']
//ifModifiedSince檔案的最新修改時間
let ifModifiedSince = req.headers['if-modified-since']
let since = statObj.ctime.toUTCString();//最新修改時間
//代表的是伺服器檔案的一個描述
let etag = new Date(since).getTime() +'-'+statObj.size
res.setHeader('Cache-Control','max-age=10')
//10秒之內強制快取
res.setHeader('Etag',etag)
res.setHeader('Last-Modified',since) //請求頭帶著
//再訪問的時候對比,如果相等,就走快取
if(ifNoneMatch !== etag){
return false
}
if(ifModifiedSince != since){
return false
}
res.statusCode = 304
res.end()
return true
}
sendFile
中加這句話
//快取
if(this.cache(req,res,statObj)) return
那麼訪問index.html
訪問的畫面是
檢視他們的頭
當然再重新整理的話200就會變成304,走的是快取了
壓縮
因為用到了zlib所以要在頭上加上
let zlib = require('zlib');
壓縮方法
compress(req,res,statObj){
// 壓縮 Accept-Encoding: gzip,deflate,br
// Content-Encoding:gzip
let header = req.headers['accept-encoding']
if(header){
if(header.match(/\bgzip\b/)){
res.setHeader('Content-Encoding','gzip')
return zlib.createGzip()
}else if(header.match(/\bdeflate\b/)){
res.setHeader('Content-Encoding','deflate')
return zlib.createDeflate()
}else{
return false //不支援壓縮
}
}else{
return false
}
}
sendFile
sendFile(req,res,p,statObj){
//快取
if(this.cache(req,res,statObj)) return
//壓縮
let s = this.compress(req, res, p, statObj);
console.log(s)
res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
let rs = fs.createReadStream(p)
if(s){
//如果支援就是返回的流
rs.pipe(s).pipe(res)
}else{
rs.pipe(res)
}
//是檔案就直接讀了
// fs.createReadStream(p).pipe(res)
}
range(req,res,statObj){
//範圍請求的頭 :Rang:bytes=1-100
//伺服器 Accept-Ranges:bytes
//Content-Ranges:1-100/total
let header = req.headers['range']
//header =>bytes=1-100
let start = 0;
let end = statObj.size;//整個檔案的大小
if(header){
res.setHeader('Content-Range','bytes')
res.setHeader('Accept-Ranges',`bytes ${start}-${end}/${statObj.size}`)
let [,s,e] = header.match(/bytes=(\d*)-(\d*)/);
start = s?parseInt(s):start
end = e? parseInt(e):end
}
return {start,end:end-1}//因為start是從0開始
}
sendFile
檔案就是這樣了
sendFile(req, res, p, statObj) {
// 快取的功能 對比 強制
if (this.cache(req, res, statObj)) return;
// 壓縮 Accept-Encoding: gzip,deflate,br
// Content-Encoding:gzip
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
let s = this.compress(req, res, p, statObj);
// 範圍請求
let {start,end} = this.range(req,res,statObj);
let rs = fs.createReadStream(p,{start,end})
if (s) {
rs.pipe(s).pipe(res);
} else {
rs.pipe(res);
}
}
在命令列工具下執行
curl -v –header “Range:bytes=1-3” http://localhost:3000/index.html
就可以看到效果了
如果你的window不能執行curl 可以看這裡
git地址