前言: 關於 http server
應該有小夥伴瞭解或用過http-server
,http-server
是一個node環境下的命令列http伺服器,這裡是npm官網的連結 www.npmjs.com/package/htt… ,
可以從npm的官網查到其用法:
即npm安裝後,在命令列輸入指令http-server
直接開啟伺服器,在伺服器啟動的目錄下,預設會找public靜態資源目錄,去訪問預設的127.0.0.1:8080 可以訪問到靜態目錄站點。
非常易用和方便,如果我們想改變埠,預設目錄,或者主機名等等,可以在啟動時在命令列直接配置
具體用法就是cmd: http-server -p 3001
那麼啟動時就會訪問3001埠,其他配置可以參考npm官網瞭解,在這裡就不贅述了。本篇主要想通過http-server的底層原理,實現一個簡易的http-server包,實現後還可以發到npm上成為自己的作品哦,一起看看吧。
npm 註冊
想要了解發布npm的同學可以在npm官網註冊屬於自己的npm賬號,注意郵箱一定要驗證哦,不然釋出不了自己的包
開始專案前最好提前瞭解的一些內容
- nodejs環境
- fs模組,包括讀取檔案,讀寫流等
- async,promise
- http知識
- ejs模板引擎
一 專案搭建
package.json
首先要安裝node環境,因為我們的專案是基於nodejs的http工具。 建立專案資料夾後用npm或yarn初始化都可以,建立專案的package.json檔案,配置過程跟隨包管理工具的提示即可,輸入name的時候,可以使用你想要釋出的包的名字;author輸入你在npm官網註冊的使用者名稱,版本就預設是1.0.0就好,我的pachage.json配置如下
這裡bin我們配置為 啟動命令列自定義名:"bin/www.js",作為我們的命令列啟動配置檔案
main: index.js是我們主要邏輯指令碼
author是作者名,這裡使用npm官網的使用者名稱保持一致即可
資料夾結構
建議大家這樣配置
首先有bin資料夾,下放www.js,這裡我們用於配置命令列工具,也是啟動包的關鍵所在
node_modules為安裝依賴後生成的,請忽略
public是我們想要讓使用者訪問的靜態資源目錄,可以隨意放一些資料夾和檔案
src是我們的核心js和配置
src/template.html是展示的介面,因為我們要搭建的是一個靜態資源站點,使用者需要訪問頁面,可以點選目錄或檔案等操作。
二 核心程式碼
1. src/config.js
首先我們構建config.js,即預設配置項
module.exports = {
port: 8080, // 預設埠
host: 'localhost', // 預設主機名
dir: process.cwd() // 預設讀取目錄
}
複製程式碼
我們匯出預設的埠號,預設的主機名,和預設的檔案目錄,
其中 process.cwd()是讀取程式當前的工作目錄,你在哪啟動,就讀哪個目錄
2. src/template.html
上面講到了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.
3. src/index.js
這裡是我們的主要邏輯指令碼了,本js裡會封裝Server類,用於在www.js中啟動伺服器
簡要架構:
我們引入的模組主要有:
- http http模組
- util util工具模組,主要使用promisify方法
- url url模組 可方便獲取路徑
- zlib 檔案壓縮模組,用於建立gzip或deflate的轉化流
- fs 檔案系統模組
- path 路徑拼接
- querystring 路徑處理
- ejs 模板引擎
- chalk 粉筆模組 可以給輸出的命令列文字加顏色喔
- mime 獲取檔案型別
- debug debug模組
- config 我們自己寫的配置檔案
下面開始構建 Server 類,用於處理請求中的各種情況,返回不同的內容
① 構造Server
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)}`);
});
}
...
}
複製程式碼
至此,我們的核心程式碼已經寫完了,簡易處理了伺服器端需要處理的一些狀況,有興趣的同學可以補充和完善
三 bin/www.js 執行指令碼
我們在命令列中輸入的指令,調器執行指令碼,並開啟伺服器
注意在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 link
處理npm install 我們包中的依賴(ejs, chalk, debug等)之外還需要執行
npm link:將一個任意位置的npm包連結到全域性執行環境,從而在任意位置使用命令列都可以直接執行該npm包
四 執行
在資料夾啟動命令列工具 執行zyx-http-server -d public
,沒有什麼問題的話,我們會彈出
點選a資料夾
開啟檔案
原始檔:至此,我們實現了傳說中的http-server靜態伺服器,由於我叫zyx啦,所以取名叫zyx-http-server
五 釋出到npm官網
沒有註冊的同學看到這一步的話請先去npm官網註冊一個屬於自己的賬號,然後我們才能釋出到自己包。
進行之前有這麼幾點需要注意
- 切換源到npm節點,如果平時使用cnpm或者其他節點的同學,請在命令列輸入
nrm use npm
切換 - 在npm的註冊郵箱一定要驗證才可以,官網會發一份驗證郵件給你,點選進行驗證;我遇到了一種情況是驗證過了,但是沒生效,這時候你去官網再改一次郵箱試試
- 要發的包的命名,是package.json中的name配置項,一開始沒有配置的,可以去寫一個自己的包名,可以通過訪問 www.npmjs.com/package/ + 你的包名,看看在npm有沒有被佔用,被佔用的話就換一個哦,一般被佔用的話,你也無法提交
npm login
需要在命令列執行 npm login
登入npm,如果你一開始沒有切換到npm官網節點,cnpm用npm賬號也是可以登入的,所以請提前先切換到npm
npm publish
登入成功後,就可以執行
npm publish
指令,釋出成功有如下提示
更新程式碼的流程和釋出是一樣的,但是你要更新package.json中的version號。 這是我的包的地址,大家有興趣可以看看 zyx-http-server
使用自己的包
依然是使用 npm i zyx-http-server -g
全域性安裝或區域性安裝這個包,我們試一下:
可以啟動
中文資料夾bug已修復
在handleRequest方法中對pathname進行轉碼處理
希望我的文章可以幫到你~