深入理解nodejs搭建靜態(tài)服務(wù)器(實(shí)現(xiàn)命令行)
靜態(tài)服務(wù)器
使用node搭建一個(gè)可在任何目錄下通過(guò)命令啟動(dòng)的一個(gè)簡(jiǎn)單http靜態(tài)服務(wù)器
安裝:npm install yg-server -g
啟動(dòng):yg-server
可通過(guò)以上命令安裝,啟動(dòng),來(lái)看一下最終的效果
TODO
- 創(chuàng)建一個(gè)靜態(tài)服務(wù)器
- 通過(guò)yargs來(lái)創(chuàng)建命令行工具
- 處理緩存
- 處理壓縮
初始化
- 創(chuàng)建目錄:mkdir static-server
- 進(jìn)入到該目錄:cd static-server
- 初始化項(xiàng)目:npm init
- 構(gòu)建文件夾目錄結(jié)構(gòu):
初始化靜態(tài)服務(wù)器
- 首先在src目錄下創(chuàng)建一個(gè)app.js
- 引入所有需要的包,非node自帶的需要npm安裝一下
- 初始化構(gòu)造函數(shù),options參數(shù)由命令行傳入,后續(xù)會(huì)講到
- this.host 主機(jī)名
- this.port 端口號(hào)
- this.rootPath 根目錄
- this.cors 是否開(kāi)啟跨域
- this.openbrowser 是否自動(dòng)打開(kāi)瀏覽器
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'); //自動(dòng)啟動(dòng)瀏覽器 const handlebars = require('handlebars'); // 模版 const templates = require('./templates'); // 用來(lái)渲染的模版文件 class StaticServer { constructor(options) { this.host = options.host; this.port = options.port; this.rootPath = process.cwd(); this.cors = options.cors; this.openbrowser = options.openbrowser; } }
處理錯(cuò)誤響應(yīng)
在寫(xiě)具體業(yè)務(wù)前,先封裝幾個(gè)處理響應(yīng)的函數(shù),分別是錯(cuò)誤的響應(yīng)處理,沒(méi)有找到資源的響應(yīng)處理,在后面會(huì)調(diào)用這么幾個(gè)函數(shù)來(lái)做響應(yīng)
- 處理錯(cuò)誤
- 返回狀態(tài)碼500
- 返回錯(cuò)誤信息
responseError(req, res, err) { res.writeHead(500); res.end(`there is something wrong in th server! please try later!`); }
- 處理資源未找到的響應(yīng)
- 返回狀態(tài)碼404
- 返回一個(gè)404html
responseNotFound(req, res) { // 這里是用handlerbar處理了一個(gè)模版并返回,這個(gè)模版只是單純的一個(gè)寫(xiě)著404html const html = handlebars.compile(templates.notFound)(); res.writeHead(404, { 'Content-Type': 'text/html' }); res.end(html); }
處理緩存
在前面的一篇文章里我介紹過(guò)node處理緩存的幾種方式,這里為了方便我只使用的協(xié)商緩存,通過(guò)ETag來(lái)做驗(yàn)證
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); }); }); }
處理壓縮
- 通過(guò)請(qǐng)求頭accept-encoding來(lái)判斷瀏覽器支持的壓縮方式
- 設(shè)置壓縮響應(yīng)頭,并創(chuàng)建對(duì)文件的壓縮方式
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; } }
啟動(dòng)靜態(tài)服務(wù)器
- 添加一個(gè)啟動(dòng)服務(wù)器的方法
- 所有請(qǐng)求都交給this.requestHandler這個(gè)函數(shù)來(lái)處理
- 端口號(hào)
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}`); }); }
請(qǐng)求處理
- 通過(guò)url模塊解析請(qǐng)求路徑,獲取請(qǐng)求資源名
- 獲取請(qǐng)求的文件路徑
- 通過(guò)fs模塊判斷文件是否存在,這里分三種情況
- 請(qǐng)求路徑是一個(gè)文件夾,則調(diào)用responseDirectory處理
- 請(qǐng)求路徑是一個(gè)文件,則調(diào)用responseFile處理
- 如果請(qǐng)求的文件不存在,則調(diào)用responseNotFound處理
requestHandler(req, res) { // 通過(guò)url模塊解析請(qǐng)求路徑,獲取請(qǐng)求文件 const { pathname } = url.parse(req.url); // 獲取請(qǐng)求的文件路徑 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); } }); }
處理請(qǐng)求的文件
- 每次返回文件前,先調(diào)用前面我們寫(xiě)的cacheHandler模塊來(lái)處理緩存
- 如果有緩存則返回304
- 如果不存在緩存,則設(shè)置文件類型,etag,跨域響應(yīng)頭
- 調(diào)用compressHandler對(duì)返回的文件進(jìn)行壓縮處理
- 返回資源
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); } ); }
處理請(qǐng)求的文件夾
- 如果客戶端請(qǐng)求的是一個(gè)文件夾,則返回的應(yīng)該是該目錄下的所有資源列表,而非一個(gè)具體的文件
- 通過(guò)fs.readdir可以獲取到該文件夾下面所有的文件或文件夾
- 通過(guò)map來(lái)獲取一個(gè)數(shù)組對(duì)象,是為了把該目錄下的所有資源通過(guò)模版去渲染返回給客戶端
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;
創(chuàng)建命令行工具
- 首先在bin目錄下創(chuàng)建一個(gè)config.js
- 導(dǎo)出一些默認(rèn)的配置
module.exports = { host: 'localhost', port: 3000, cors: true, openbrowser: true, index: 'index.html', charset: 'utf8' };
- 然后創(chuàng)建一個(gè)static-server.js
- 這里設(shè)置的是一些可執(zhí)行的命令
- 并實(shí)例化了我們最初在app.js里寫(xiě)的server類,將options作為參數(shù)傳入
- 最后調(diào)用server.start()來(lái)啟動(dòng)我們的服務(wù)器
- 注意 #! /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: '設(shè)置服務(wù)器端口號(hào)', type: 'number', default: config.port }) .option('o', { alias: 'openbrowser', describe: '是否打開(kāi)瀏覽器', type: 'boolean', default: config.openbrowser }) .option('n', { alias: 'host', describe: '設(shè)置主機(jī)名', 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', '在根目錄開(kāi)啟8000端口的靜態(tài)服務(wù)器') .help('h').argv; const server = new StaticServer(options); server.start();
入口文件
最后回到根目錄下的index.js,將我們的模塊導(dǎo)出,這樣可以在根目錄下通過(guò)node index來(lái)調(diào)試
module.exports = require('./bin/static-server');
配置命令
配置命令非常簡(jiǎn)單,進(jìn)入到package.json文件里
加入一句話
"bin": { "yg-server": "bin/static-server.js" },
- yg-server是啟動(dòng)該服務(wù)器的命令,可以自己定義
- 然后執(zhí)行npm link生成一個(gè)符號(hào)鏈接文件
- 這樣你就可以通過(guò)命令來(lái)執(zhí)行自己的服務(wù)器了
- 或者將包托管到npm上,然后全局安裝,在任何目錄下你都可以通過(guò)你設(shè)置的命令來(lái)開(kāi)啟一個(gè)靜態(tài)服務(wù)器,在我們平時(shí)總會(huì)需要這樣一個(gè)靜態(tài)服務(wù)器
總結(jié)
寫(xiě)到這里基本上就寫(xiě)完了,另外還有幾個(gè)模版文件,是用來(lái)在客戶端展示的,可以看我的github,我就不貼了,只是一些html而已,你也可以自己設(shè)置,這個(gè)博客寫(xiě)多了是在是太卡了,字都打不動(dòng)了。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
nodejs獲取表單數(shù)據(jù)的三種方法實(shí)例
在開(kāi)發(fā)中經(jīng)常需要獲取form表單的數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于nodejs獲取表單數(shù)據(jù)的三種方法,方法分別是form表單傳遞、ajax請(qǐng)求傳遞以及表單序列化,需要的朋友可以參考下2021-06-06node將對(duì)象轉(zhuǎn)化為query的實(shí)現(xiàn)方法
本文主要介紹了node將對(duì)象轉(zhuǎn)化為query的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01Node.js數(shù)據(jù)流Stream之Readable流和Writable流用法
這篇文章介紹了Node.js數(shù)據(jù)流Stream之Readable流和Writable流的用法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Express進(jìn)階之log4js實(shí)用入門(mén)指南
本篇文章主要介紹了Express進(jìn)階之log4js實(shí)用入門(mén)指南,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-02-02npm install安裝模塊-save和-save-dev命令的區(qū)別
這篇文章介紹了npm install安裝模塊-save和-save-dev命令的區(qū)別,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06nodejs實(shí)現(xiàn)百度輿情接口應(yīng)用示例
這篇文章主要介紹了nodejs實(shí)現(xiàn)百度輿情接口應(yīng)用,結(jié)合實(shí)例形式分析了node.js調(diào)用百度輿情接口的具體使用技巧,需要的朋友可以參考下2020-02-02node.js中的fs.writeFile方法使用說(shuō)明
這篇文章主要介紹了node.js中的fs.writeFile方法使用說(shuō)明,本文介紹了fs.writeFile的方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12NodeJs實(shí)現(xiàn)簡(jiǎn)易WEB上傳下載服務(wù)器
這篇文章主要為大家詳細(xì)介紹了NodeJs實(shí)現(xiàn)一個(gè)簡(jiǎn)易WEB上傳下載服務(wù)器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08