深入nodejs-搭建靜態伺服器(實現命令列)
靜態伺服器
使用node搭建一個可在任何目錄下通過命令啟動的一個簡單http靜態伺服器
可通過以上命令安裝,啟動,來看一下最終的效果
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' };
#! /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" },
npm link
總結
寫到這裡基本上就寫完了,另外還有幾個模版檔案,是用來在客戶端展示的,可以看我的 github ,我就不貼了,只是一些html而已,你也可以自己設定,這個部落格寫多了是在是太卡了,字都打不動了。
另外有哪裡寫的不好的地方或看不懂的地方可以給我留言。
個人公眾號歡迎關注