靜態服務器
使用node搭建一個可在任何目錄下通過命令啓動的一個簡單http靜態服務器
完整代碼鏈接
安裝:npm install yg-server -g
啓動:yg-server
可通過以上命令安裝,啓動,來看一下最終的效果
TODO
- 創建一個靜態服務器
- 通過yargs來創建命令行工具
- 處理緩存
- 處理壓縮
初始化
- 創建目錄:
mkdir static-server
- 進入到該目錄:
cd static-server
- 初始化項目:
npm init
- 構建文件夾目錄結構:
初始化靜態服務器
- 首先在src目錄下創建一個app.js
- 引入所有需要的包,非node自帶的需要npm安裝一下
-
初始化構造函數,options參數由命令行傳入,後續會講到
- this.host 主機名
- this.port 端口號
- this.rootPath 根目錄
- this.cors 是否開啓跨域
- this.openbrowser 是否自動打開瀏覽器
const http = require('http'); // http模塊
const url = require('url'); // 解析路徑
const path = require('path'); // path模塊
const fs = require('fs'); // 文件處理模塊
const mime = require('mime'); // 解析文件類型
const crypto = require('crypto'); // 加密模塊
const zlib = require('zlib'); // 壓縮
const openbrowser = require('open'); //自動啓動瀏覽器
const handlebars = require('handlebars'); // 模版
const templates = require('./templates'); // 用來渲染的模版文件
class StaticServer {
constructor(options) {
this.host = options.host;
this.port = options.port;
this.rootPath = process.cwd();
this.cors = options.cors;
this.openbrowser = options.openbrowser;
}
}
處理錯誤響應
在寫具體業務前,先封裝幾個處理響應的函數,分別是錯誤的響應處理,沒有找到資源的響應處理,在後面會調用這麼幾個函數來做響應
- 處理錯誤
- 返回狀態碼500
- 返回錯誤信息
responseError(req, res, err) {
res.writeHead(500);
res.end(`there is something wrong in th server! please try later!`);
}
- 處理資源未找到的響應
- 返回狀態碼404
- 返回一個404html
responseNotFound(req, res) {
// 這裏是用handlerbar處理了一個模版並返回,這個模版只是單純的一個寫着404html
const html = handlebars.compile(templates.notFound)();
res.writeHead(404, {
'Content-Type': 'text/html'
});
res.end(html);
}
處理緩存
在前面的一篇文章裏我介紹過node處理緩存的幾種方式,這裏爲了方便我只使用的協商緩存,通過ETag來做驗證
cacheHandler(req, res, filepath) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filepath);
const md5 = crypto.createHash('md5');
const ifNoneMatch = req.headers['if-none-match'];
readStream.on('data', data => {
md5.update(data);
});
readStream.on('end', () => {
let etag = md5.digest('hex');
if (ifNoneMatch === etag) {
resolve(true);
}
resolve(etag);
});
readStream.on('error', err => {
reject(err);
});
});
}
處理壓縮
- 通過請求頭accept-encoding來判斷瀏覽器支持的壓縮方式
- 設置壓縮響應頭,並創建對文件的壓縮方式
compressHandler(req, res) {
const acceptEncoding = req.headers['accept-encoding'];
if (/\bgzip\b/.test(acceptEncoding)) {
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
} else if (/\bdeflate\b/.test(acceptEncoding)) {
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
} else {
return false;
}
}
啓動靜態服務器
- 添加一個啓動服務器的方法
- 所有請求都交給this.requestHandler這個函數來處理
- 監聽端口號
start() {
const server = http.createSercer((req, res) => this.requestHandler(req, res));
server.listen(this.port, () => {
if (this.openbrowser) {
openbrowser(`http://${this.host}:${this.port}`);
}
console.log(`server started in http://${this.host}:${this.port}`);
});
}
請求處理
- 通過url模塊解析請求路徑,獲取請求資源名
- 獲取請求的文件路徑
-
通過fs模塊判斷文件是否存在,這裏分三種情況
- 請求路徑是一個文件夾,則調用responseDirectory處理
- 請求路徑是一個文件,則調用responseFile處理
- 如果請求的文件不存在,則調用responseNotFound處理
requestHandler(req, res) {
// 通過url模塊解析請求路徑,獲取請求文件
const { pathname } = url.parse(req.url);
// 獲取請求的文件路徑
const filepath = path.join(this.rootPath, pathname);
// 判斷文件是否存在
fs.stat(filepath, (err, stat) => {
if (!err) {
if (stat.isDirectory()) {
this.responseDirectory(req, res, filepath, pathname);
} else {
this.responseFile(req, res, filepath, stat);
}
} else {
this.responseNotFound(req, res);
}
});
}
處理請求的文件
- 每次返回文件前,先調用前面我們寫的cacheHandler模塊來處理緩存
- 如果有緩存則返回304
- 如果不存在緩存,則設置文件類型,etag,跨域響應頭
- 調用compressHandler對返回的文件進行壓縮處理
- 返回資源
responseFile(req, res, filepath, stat) {
this.cacheHandler(req, res, filepath).then(
data => {
if (data === true) {
res.writeHead(304);
res.end();
} else {
res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
res.setHeader('Etag', data);
this.cors && res.setHeader('Access-Control-Allow-Origin', '*');
const compress = this.compressHandler(req, res);
if (compress) {
fs.createReadStream(filepath)
.pipe(compress)
.pipe(res);
} else {
fs.createReadStream(filepath).pipe(res);
}
}
},
error => {
this.responseError(req, res, error);
}
);
}
處理請求的文件夾
- 如果客戶端請求的是一個文件夾,則返回的應該是該目錄下的所有資源列表,而非一個具體的文件
- 通過fs.readdir可以獲取到該文件夾下面所有的文件或文件夾
- 通過map來獲取一個數組對象,是爲了把該目錄下的所有資源通過模版去渲染返回給客戶端
responseDirectory(req, res, filepath, pathname) {
fs.readdir(filepath, (err, files) => {
if (!err) {
const fileList = files.map(file => {
const isDirectory = fs.statSync(filepath + '/' + file).isDirectory();
return {
filename: file,
url: path.join(pathname, file),
isDirectory
};
});
const html = handlebars.compile(templates.fileList)({ title: pathname, fileList });
res.setHeader('Content-Type', 'text/html');
res.end(html);
}
});
app.js完整代碼
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const crypto = require('crypto');
const zlib = require('zlib');
const openbrowser = require('open');
const handlebars = require('handlebars');
const templates = require('./templates');
class StaticServer {
constructor(options) {
this.host = options.host;
this.port = options.port;
this.rootPath = process.cwd();
this.cors = options.cors;
this.openbrowser = options.openbrowser;
}
/**
* handler request
* @param {*} req
* @param {*} res
*/
requestHandler(req, res) {
const { pathname } = url.parse(req.url);
const filepath = path.join(this.rootPath, pathname);
// To check if a file exists
fs.stat(filepath, (err, stat) => {
if (!err) {
if (stat.isDirectory()) {
this.responseDirectory(req, res, filepath, pathname);
} else {
this.responseFile(req, res, filepath, stat);
}
} else {
this.responseNotFound(req, res);
}
});
}
/**
* Reads the contents of a directory , response files list to client
* @param {*} req
* @param {*} res
* @param {*} filepath
*/
responseDirectory(req, res, filepath, pathname) {
fs.readdir(filepath, (err, files) => {
if (!err) {
const fileList = files.map(file => {
const isDirectory = fs.statSync(filepath + '/' + file).isDirectory();
return {
filename: file,
url: path.join(pathname, file),
isDirectory
};
});
const html = handlebars.compile(templates.fileList)({ title: pathname, fileList });
res.setHeader('Content-Type', 'text/html');
res.end(html);
}
});
}
/**
* response resource
* @param {*} req
* @param {*} res
* @param {*} filepath
*/
async responseFile(req, res, filepath, stat) {
this.cacheHandler(req, res, filepath).then(
data => {
if (data === true) {
res.writeHead(304);
res.end();
} else {
res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
res.setHeader('Etag', data);
this.cors && res.setHeader('Access-Control-Allow-Origin', '*');
const compress = this.compressHandler(req, res);
if (compress) {
fs.createReadStream(filepath)
.pipe(compress)
.pipe(res);
} else {
fs.createReadStream(filepath).pipe(res);
}
}
},
error => {
this.responseError(req, res, error);
}
);
}
/**
* not found request file
* @param {*} req
* @param {*} res
*/
responseNotFound(req, res) {
const html = handlebars.compile(templates.notFound)();
res.writeHead(404, {
'Content-Type': 'text/html'
});
res.end(html);
}
/**
* server error
* @param {*} req
* @param {*} res
* @param {*} err
*/
responseError(req, res, err) {
res.writeHead(500);
res.end(`there is something wrong in th server! please try later!`);
}
/**
* To check if a file have cache
* @param {*} req
* @param {*} res
* @param {*} filepath
*/
cacheHandler(req, res, filepath) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filepath);
const md5 = crypto.createHash('md5');
const ifNoneMatch = req.headers['if-none-match'];
readStream.on('data', data => {
md5.update(data);
});
readStream.on('end', () => {
let etag = md5.digest('hex');
if (ifNoneMatch === etag) {
resolve(true);
}
resolve(etag);
});
readStream.on('error', err => {
reject(err);
});
});
}
/**
* compress file
* @param {*} req
* @param {*} res
*/
compressHandler(req, res) {
const acceptEncoding = req.headers['accept-encoding'];
if (/\bgzip\b/.test(acceptEncoding)) {
res.setHeader('Content-Encoding', 'gzip');
return zlib.createGzip();
} else if (/\bdeflate\b/.test(acceptEncoding)) {
res.setHeader('Content-Encoding', 'deflate');
return zlib.createDeflate();
} else {
return false;
}
}
/**
* server start
*/
start() {
const server = http.createServer((req, res) => this.requestHandler(req, res));
server.listen(this.port, () => {
if (this.openbrowser) {
openbrowser(`http://${this.host}:${this.port}`);
}
console.log(`server started in http://${this.host}:${this.port}`);
});
}
}
module.exports = StaticServer;
創建命令行工具
- 首先在bin目錄下創建一個config.js
- 導出一些默認的配置
module.exports = {
host: 'localhost',
port: 3000,
cors: true,
openbrowser: true,
index: 'index.html',
charset: 'utf8'
};
- 然後創建一個static-server.js
- 這裏設置的是一些可執行的命令
- 並實例化了我們最初在app.js裏寫的server類,將options作爲參數傳入
- 最後調用server.start()來啓動我們的服務器
- 注意
#! /usr/bin/env node
這一行不能省略哦
#! /usr/bin/env node
const yargs = require('yargs');
const path = require('path');
const config = require('./config');
const StaticServer = require('../src/app');
const pkg = require(path.join(__dirname, '..', 'package.json'));
const options = yargs
.version(pkg.name + '@' + pkg.version)
.usage('yg-server [options]')
.option('p', { alias: 'port', describe: '設置服務器端口號', type: 'number', default: config.port })
.option('o', { alias: 'openbrowser', describe: '是否打開瀏覽器', type: 'boolean', default: config.openbrowser })
.option('n', { alias: 'host', describe: '設置主機名', type: 'string', default: config.host })
.option('c', { alias: 'cors', describe: '是否允許跨域', type: 'string', default: config.cors })
.option('v', { alias: 'version', type: 'string' })
.example('yg-server -p 8000 -o localhost', '在根目錄開啓監聽8000端口的靜態服務器')
.help('h').argv;
const server = new StaticServer(options);
server.start();
入口文件
最後回到根目錄下的index.js,將我們的模塊導出,這樣可以在根目錄下通過node index
來調試
module.exports = require('./bin/static-server');
配置命令
配置命令非常簡單,進入到package.json文件裏
加入一句話
"bin": {
"yg-server": "bin/static-server.js"
},
- yg-server是啓動該服務器的命令,可以自己定義
- 然後執行
npm link
生成一個符號鏈接文件 - 這樣你就可以通過命令來執行自己的服務器了
- 或者將包託管到npm上,然後全局安裝,在任何目錄下你都可以通過你設置的命令來開啓一個靜態服務器,在我們平時總會需要這樣一個靜態服務器
總結
寫到這裏基本上就寫完了,另外還有幾個模版文件,是用來在客戶端展示的,可以看我的github,我就不貼了,只是一些html而已,你也可以自己設置,這個博客寫多了是在是太卡了,字都打不動了。
另外有哪裏寫的不好的地方或看不懂的地方可以給我留言。如果你覺得還有點用,給我github這個上點個star我會很感激你的哈
個人公衆號歡迎關注