應該有小夥伴瞭解或用過http-server
,http-server
是一個node環境下的命令行http服務器,這裏是npm官網的連接 www.npmjs.com/package/htt… , 能夠從npm的官網查到其用法:
即npm安裝後,在命令行輸入指令http-server
直接開啓服務器,在服務器啓動的目錄下,默認會找public靜態資源目錄,去訪問默認的127.0.0.1:8080 能夠訪問到靜態目錄站點。css
很是易用和方便,若是咱們想改變端口,默認目錄,或者主機名等等,能夠在啓動時在命令行直接配置html
cmd: http-server -p 3001
那麼啓動時就會訪問3001端口,其餘配置能夠參考npm官網瞭解,在這裏就不贅述了。本篇主要想經過http-server的底層原理,實現一個簡易的http-server包,實現後還能夠發到npm上成爲本身的做品哦,一塊兒看看吧。
想要了解發布npm的同窗能夠在npm官網註冊屬於本身的npm帳號,注意郵箱必定要驗證哦,否則發佈不了本身的包node
首先要安裝node環境,由於咱們的項目是基於nodejs的http工具。 建立項目文件夾後用npm或yarn初始化均可以,建立項目的package.json文件,配置過程跟隨包管理工具的提示便可,輸入name的時候,可使用你想要發佈的包的名字;author輸入你在npm官網註冊的用戶名,版本就默認是1.0.0就好,個人pachage.json配置以下npm
這裏bin咱們配置爲 啓動命令行自定義名:"bin/www.js",做爲咱們的命令行啓動配置文件
main: index.js是咱們主要邏輯腳本
author是做者名,這裏使用npm官網的用戶名保持一致便可json
建議你們這樣配置 首先有bin文件夾,下放www.js,這裏咱們用於配置命令行工具,也是啓動包的關鍵所在
node_modules爲安裝依賴後生成的,請忽略
public是咱們想要讓用戶訪問的靜態資源目錄,能夠隨意放一些文件夾和文件
src是咱們的核心js和配置
src/template.html是展現的界面,由於咱們要搭建的是一個靜態資源站點,用戶須要訪問頁面,能夠點擊目錄或文件等操做。windows
首先咱們構建config.js,即默認配置項數組
module.exports = {
port: 8080, // 默認端口
host: 'localhost', // 默認主機名
dir: process.cwd() // 默認讀取目錄
}
複製代碼
咱們導出默認的端口號,默認的主機名,和默認的文件目錄,
其中 process.cwd()是讀取進程當前的工做目錄,你在哪啓動,就讀哪一個目錄promise
上面講到了template.html用於展現目錄,咱們這裏採用ejs模板引擎,服務器端渲染的方式展現。 這裏有ejs介紹,能夠簡單瞭解寫法。瀏覽器
<!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>
<h2><%=name%></h2>
<%arr.forEach(item=>{%>
<li><a href="<%=item.href%>"><%=item.name%></a></li>
<%})%>
</body>
</html>
複製代碼
h2
標籤中咱們展現當前路徑 li
爲文件目錄結構,可供點擊進入文件或文件夾;能夠看出,咱們要給這個頁面輸出name和arr兩個數據,在覈心js中會詳細講如何渲染template.緩存
這裏是咱們的主要邏輯腳本了,本js裏會封裝Server類,用於在www.js中啓動服務器
簡要架構:
咱們引入的模塊主要有:
class Server {
constructor(command) {
this.config = {...config,...command} // config和命令行的內容展現
this.template = template;
}
}
複製代碼
class Server {
...
async handleRequest(req, res) {
let { dir } = this.config; // 須要將請求的路徑和dir拼接在一塊兒
//如 http://localhost:8080/index.html
let { pathname } = url.parse(req.url);
// 若是獨到的是網站小圖標,就直接輸出
if (pathname === '/favicon.ico') return res.end();
pathname = decodeURIComponent(pathname); // 對文件夾名稱進行轉碼處理
// p是決定文件路徑
let p = path.join(dir, pathname);
try {
// 判斷當前路徑是文件 仍是文件夾
let statObj = await stat(p);
if (statObj.isDirectory()) {
// 讀取當前訪問的目錄下的全部內容 readdir 數組 把數組渲染回頁面
res.setHeader('Content-Type', 'text/html;charset=utf8')
let dirs = await readdir(p);
dirs = dirs.map(item=>({
name:item,
// 由於點擊第二層時 須要帶上第一層的路徑,全部拼接上就ok了
href:path.join(pathname,item)
}))
// 渲染template.html中須要填充的內容,name是當前文件目錄,arr爲當前文件夾下的目錄數組
let str = ejs.render(this.template, {
name: `Index of ${pathname}`,
arr: dirs
});
// 響應中返回填充內容
res.end(str);
} else {
// 若是不是文件夾,則直接輸出文件內容
this.sendFile(req, res, statObj, p);
}
} catch (e) {
debug(e); // 發送錯誤
this.sendError(req, res);
}
}
...
}
複製代碼
用於告知服務器是否本次緩存,若是是瀏覽器客戶端已經緩存的文件,直接讀取緩存便可,優化性能
class Server {
...
cache(req, res, statObj, p ) {
// 設置緩存頭
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).getTime());
// 設置etag和上次最新修改時間
let eTag = statObj.ctime.getTime() + '-' + statObj.size;
let lastModified = statObj.ctime.getTime();
// 傳給客戶端
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 && lastModified !== ifModifiedSince) {
return false;
}
return true;
}
...
}
複製代碼
返回壓縮文件,優化訪問速度
class Server {
...
// 是否壓縮
gzip(req, res, statObj, p) {
// 判斷請求頭是否設置了接收編碼
let encoding = req.headers['accept-encoding'];
// 若是有則判斷是否有gzip或者deflate
if (encoding) {
// gzip
if (encoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
}
// deflate
if (encoding.match(/\bdeflate\b/)) {
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
}
return false;
}
else {
return false;
}
}
...
}
複製代碼
判斷是否請求頭
class Server {
...
range(req, res, statObj, p) {
let range = req.headers['range'];
// 有範圍請求時返回讀流,斷點續傳
if (range) {
let [, start, end] = range.match(/bytes=(\d*)-(\d*)/);
start = start ? Number(start) : 0;
end = end ? Number(end) : statObj.size - 1;
res.statusCode = 206;
res.setHeader('Content-Range', `bytes ${start}-${end}/${statObj.size - 1}`);
fs.createReadStream(p, {start, end}).pipe(res);
}
else {
return false;
}
}
...
}
複製代碼
發送文件方法,即咱們在 handleRequest時若是判斷走到認爲讀取的是一個文件,則發送這個文件展現給用戶
class Server {
...
sendFile(req, res, statObj, p) {
if (this.cache(req, res, statObj, p)) {
res.statusCode = 304;
return res.end();
}
// 是範圍請求就忽略
if (this.range(req, res, statObj, p)) return;
// 設置文件類型頭,若是不設置,咱們訪問一個html文件可能會致使下載
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
// 若是是須要壓縮則定義gzip轉化流,講文件壓縮後輸出
let transform = this.gzip(req, res, statObj, p);
if (transform) {
return fs.createReadStream(p).pipe(transform).pipe(res);
}
// 若是不是不須要壓縮則直接返回文件
fs.createReadStream(p).pipe(res);
}
...
}
複製代碼
在handleRequest方法中的處理錯誤方法,
class Server {
...
sendError(req, res){
// 返回的狀態碼設置爲404
res.statusCode = 404;
// 頁面返回文字
res.end(`404 Not Found`);
this.start();
}
...
}
複製代碼
原生http模塊的建立服務器方法,建立成功後再cmd界面輸出,告知主機和端口
class Server {
...
start() {
let server = http.createServer(this.handleRequest.bind(this));
server.listen(this.config.port, this.config.host, ()=> {
console.log(`server start http://${this.config.host}:${chalk.green(this.config.port)}`);
});
}
...
}
複製代碼
咱們在命令行中輸入的指令,調器執行腳本,並開啓服務器
注意在www.js開頭必定要寫 #! /usr/bin/env node
告知操做系統node環境執行,如下爲www.js的內容
#! /usr/bin/env node
let Server = require('../src/index.js'); // 導入Server
let commander = require('commander'); // 導入命令行模塊
let {version} = require('../package.json'); // 讀取package.json的版本
// 配置命令行
commander
.option('-p,--port <n>', 'config port') // 配置端口
.option('-o,--host [value]', 'config hostname') // 配置主機名
.option('-d,--dir [value]', 'config directory') // 配置訪問目錄
.version(version, '-v,--version').parse(process.argv); // 展現版本
let server = new Server(commander);
server.start(); // 啓動
let config =require('../src/config');
commander = {...config, ...commander}
let os = require('os');
// 執行模塊
let {exec} = require('child_process')
// 判斷操做系統平臺,win32是windows,執行訪問程序,會自動彈出默認瀏覽器喔
if (os.platform() === 'win32') {
exec(`start http://${commander.host}:${commander.port}`);
}
else {
exec(`open http://${commander.host}:${commander.port}`);
}
複製代碼
再看一下咱們在package.json中的配置
"bin": {
"zyx-http-server": "bin/www.js"
},
複製代碼
即咱們的啓動命令是 zyx-http-server,也能夠根據本身的配置,補充其餘命令,如 zyx-http-server -d public
,則讀取public做爲靜態資源根目錄
處理npm install 咱們包中的依賴(ejs, chalk, debug等)以外還須要執行
npm link:將一個任意位置的npm包連接到全局執行環境,從而在任意位置使用命令行均可以直接運行該npm包
在文件夾啓動命令行工具 執行zyx-http-server -d public
,沒有什麼問題的話,咱們會彈出
打開文件
至此,咱們實現了傳說中的http-server靜態服務器,因爲我叫zyx啦,因此取名叫zyx-http-server
沒有註冊的同窗看到這一步的話請先去npm官網註冊一個屬於本身的帳號,而後咱們才能發佈到本身包。
進行以前有這麼幾點須要注意
nrm use npm
切換須要在命令行執行 npm login
登陸npm,若是你一開始沒有切換到npm官網節點,cnpm用npm帳號也是能夠登陸的,因此請提早先切換到npm
登陸成功後,就能夠執行
npm publish
指令,發佈成功有以下提示
依然是使用 npm i zyx-http-server -g
全局安裝或局部安裝這個包,咱們試一下:
能夠啓動
在handleRequest方法中對pathname進行轉碼處理
但願個人文章能夠幫到你~