本文主要写一个完整的静态服务(命令行工具),http压缩、缓存以及范围请求。 大致功能是:在当前目录启动一个服务,服务用来监听请求,处理请求。并在浏览器上渲染出当前服务下的所有目录
1.配置一个服务参数对象let path = require('path');let config = { hostname:'localhost', port:3000, dir:path.join(__dirname,'..','public')}module.exports = config;复制代码
2.写出服务的基本框架
let config = require('./config');let template = fs.readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8');class Server { constructor(args) { this.config = config;// 将配置挂载在我们的实例上 this.template = template; //用于渲染当前服务下的所有目录 } //处理请求 handleRequest() { } start() { let server = http.createServer(this.handleRequest.bind(this)); let { hostname, port } = this.config; server.listen(port, hostname); }}复制代码
这样一个服务就可以开启了,下面写如何处理请求。 处理请求主要功能是:接收请求后,解析当前请求路径下,判断该路径是否是目录,如果是,则解析该目录下所有子目录,渲染到页面上,如果不是,则直接渲染该文件。为了方便处理,我们使用async+await实现路径解析,代码如下:
async handleRequest(req, res) { let { pathname } = url.parse(req.url, true); let p = path.join(this.config.dir, pathname); // 1.根据路径 如果是文件夹 显示文件夹里的内容 // 2.如果是文件 显示文件的内容 try { // 如果没错误说明文件存在 let statObj = await stat(p); if (statObj.isDirectory()) { // 现在需要一个当前目录下的解析出的对象或者数组 let dirs = await readdir(p); dirs = dirs.map(dir => { // dirs就是要渲染的数据 return { filename: dir, pathname: path.join(pathname, dir) } }); let str = ejs.render(this.template, { dirs, title: 'ejs' }); res.setHeader('Content-Type', 'text/html;charset=utf8'); res.end(str); } else { // 文件 发送文件 this.sendFile(req, res, p, statObj); } } catch (e) { // 文件不存在的情况 //错误 } }复制代码
其中用到的模版功能是将当前目录下的子文件,渲染到页面上,代码如下:
<%=title%> <%dirs.map(item=>{%>
这样一个基本的http服务就成形了,结下来我们实现sendFile功能,主要包括:缓存、压缩、范围请求。 缓存:主要包括强制缓存和对比缓存。代码如下:
cache(req, res, p, stat) { // 实现缓存 let since = req.headers['if-modified-since']; let match = req.headers['if-none-match']; let ssince = stat.ctime.toUTCString();//文件修改时间 let smatch = stat.ctime.getTime() + stat.size;//文件修改时间和文件大小 res.setHeader('Last-Modified', ssince); res.setHeader('ETag', smatch); res.setHeader('Cache-Control','max-age=6');//强制缓存只要它成立,就走缓存 if (since != ssince) { // if-modified-since和last-modified return false; } if (match != smatch) { return false } return true; }复制代码
实现范围请求:主要根据 请求头Range,如Range:bytes=0-3表示请求前四个字节,将响应发给浏览器时,需要设置响应头 Accept-Ranges: bytes
Content-Length: 4 Content-Range: bytes 0-3/7877 设置状态码206range(req, res, p, stat) { let range = req.headers['range']; if(range){ let [, start, end] = range.match(/(\d*)-(\d*)/) || []; start = start ? parseInt(start) : 0; end = end ? parseInt(end) : stat.size; res.statusCode = 206; res.setHeader('Accept-Ranges', 'bytes'); res.setHeader('Content-Length', end - start + 1); res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`); return { start, end }; }else{ return {start:0,end:stat.size} } }复制代码
文件压缩功能:判断请求头中的Accept-Encoding是否有gzip,deflate等,有就压缩,并设置响应头Content-Encoding
// 实现服务端压缩 gzip(req, res, p, stat) { let header = req.headers['accept-encoding']; if (header) { if (header.match(/\bgzip\b/)) { res.setHeader('Content-Encoding', 'gzip'); fs.createReadStream(p).pipe(zlib.createGzip()).pipe(res); } else if (header.match(/\bdeflate\b/)) { res.setHeader('Content-Encoding', 'deflate'); fs.createReadStream(p).pipe(zlib.createDeflate()).pipe(res); } } else { fs.createReadStream(p).pipe(res); } }复制代码
然后综合缓存,压缩,范围请求,整理下sendFile方法
sendFile(req, res, p, stat) { if (this.cache(req, res, p, stat)) {// 检测是否有缓存 res.statusCode = 304; res.end(); return }; this.compressAndRange(req, res, p, stat); } compressAndRange(req, res, p, stat) { let compress = this.gzip(req, res, p, stat); let { start, end } = this.range(req, res, p, stat); //范围请求,返回开始位置和结束位置 if (compress) { // 返回的是一个压缩流 res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8'); fs.createReadStream(p, { start, end }).pipe(compress).pipe(res); } else { res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8'); fs.createReadStream(p, { start, end }).pipe(res); } }复制代码
这样服务器的功能都具备了,在实现一个命令行工具 在package.json中,添加:
"bin": { "my-http-server": "bin/www.js" }复制代码
然后npm link
在bin/www.js 中启动http server,这样就可以通过my-http-server 命令启动http服务器. 写www.js#! /usr/bin/env nodelet Server = require('../src/app');new Server().start(); // 开启服务复制代码
这样就可以通过my-http-server启动服务,但是如果我想在任意目录任意端口,任意域名下访问呢。 使用yargs包,通过命令行参数控制
#! /usr/bin/env node// 第一执行了命令后 会执行 bin/www.jslet yargs = require('yargs')let argv = yargs.option('port', {//配置端口号 alias: 'p', default: 3000, demand: false, description: 'this is port'}).option('hostname', {//配置域名 alias: 'host', default: 'localhost', type: String, demand: false, description: 'this is hostname'}).option('dir', {//配置目录 alias: 'd', default: process.cwd(),//命令的打开目录,任意目录 type: String, demand: false, description: 'this is cwd'}).usage('zf-http-server [options]').argv;let Server = require('../src/app');new Server(argv).start(); // 开启服务复制代码
注意这里将参数argv传入了Server类,然后需要修改下Server
class Server { constructor(args) { this.config = { ...config, ...args };// 将命令行中的参数传给server this.template = template; } ... ...}复制代码
这样我们通过:my-http-server -host localhost -p 3003 -d './public'命令就可以指定端口(3003),目录(当前命令所在目录/public),域名(localhost)启动服务